chore(rln): update ABI and contract address to Linea Sepolia, enhancements (#2294)

* chore: update ABI

* chore: update contract address and chain ID to Linea Sepolia

* chore: improve error handling

* fix: bigint conversion

* chore: update tests

* chore: clean comments

* chore: export keystore types

* chore: update README with contract address

* tests: add reusable mock functions

* chore: LINEA_CONTRACT instead of SEPOLIA_CONTRACT

* chore: add linea to cspell

* chore: add rateLimit to MembershipInfo

* fix(tests): rate limit additions

* chore: update gitignore
This commit is contained in:
Danish Arora 2025-03-31 14:32:29 +05:30 committed by GitHub
parent ea6daae927
commit a8ff776962
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1747 additions and 1282 deletions

View File

@ -8,8 +8,8 @@
"ahadns", "ahadns",
"Alives", "Alives",
"alphabeta", "alphabeta",
"Arraylike",
"arrayify", "arrayify",
"Arraylike",
"asym", "asym",
"autoshard", "autoshard",
"autosharding", "autosharding",
@ -70,6 +70,7 @@
"libauth", "libauth",
"libp", "libp",
"lightpush", "lightpush",
"LINEA",
"livechat", "livechat",
"Merkle", "Merkle",
"mkdir", "mkdir",

5
.gitignore vendored
View File

@ -12,4 +12,7 @@ docs
test-results test-results
playwright-report playwright-report
example example
packages/discovery/mock_local_storage packages/discovery/mock_local_storage
.cursorrules
.giga
.cursor

1008
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,11 @@ import { RLN } from '@waku/rln';
// Usage examples coming soon // Usage examples coming soon
``` ```
## Constants
- Implementation contract: 0xde2260ca49300357d5af4153cda0d18f7b3ea9b3
- Proxy contract: 0xb9cd878c90e49f797b4431fbf4fb333108cb90e6
## License ## License
MIT OR Apache-2.0 MIT OR Apache-2.0

View File

@ -59,14 +59,9 @@
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"@waku/build-utils": "^1.0.0", "@waku/build-utils": "^1.0.0",
"@waku/message-encryption": "^0.0.32", "@waku/message-encryption": "^0.0.32",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
"chai-subset": "^1.6.0",
"deep-equal-in-any-order": "^2.0.6", "deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2", "fast-check": "^3.23.2",
"rollup-plugin-copy": "^3.5.0", "rollup-plugin-copy": "^3.5.0"
"sinon": "^19.0.2"
}, },
"files": [ "files": [
"dist", "dist",
@ -86,6 +81,11 @@
"ethereum-cryptography": "^3.1.0", "ethereum-cryptography": "^3.1.0",
"ethers": "^5.7.2", "ethers": "^5.7.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"uuid": "^11.0.5" "uuid": "^11.0.5",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
"chai-subset": "^1.6.0",
"sinon": "^19.0.2"
} }
} }

View File

@ -3,7 +3,6 @@ import {
createEncoder, createEncoder,
DecodedMessage DecodedMessage
} from "@waku/core/lib/message/version_0"; } from "@waku/core/lib/message/version_0";
import type { IProtoMessage } from "@waku/interfaces";
import { import {
generatePrivateKey, generatePrivateKey,
generateSymmetricKey, generateSymmetricKey,
@ -25,40 +24,30 @@ import {
RLNDecoder, RLNDecoder,
RLNEncoder RLNEncoder
} from "./codec.js"; } from "./codec.js";
import { createRLN } from "./create.js"; import {
createTestMetaSetter,
createTestRLNCodecSetup,
EMPTY_PROTO_MESSAGE,
TEST_CONSTANTS,
verifyRLNMessage
} from "./codec.test-utils.js";
import { RlnMessage } from "./message.js"; import { RlnMessage } from "./message.js";
import { epochBytesToInt } from "./utils/index.js"; import { epochBytesToInt } from "./utils/index.js";
const TestContentTopic = "/test/1/waku-message/utf8";
const EMPTY_PUBSUB_TOPIC = "";
const EMPTY_PROTO_MESSAGE = {
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined
};
describe("RLN codec with version 0", () => { describe("RLN codec with version 0", () => {
it("toWire", async function () { it("toWire", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = createRLNEncoder({ const rlnEncoder = createRLNEncoder({
encoder: createEncoder({ contentTopic: TestContentTopic }), encoder: createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance, rlnInstance,
index, index,
credential credential
}); });
const rlnDecoder = createRLNDecoder({ const rlnDecoder = createRLNDecoder({
rlnInstance, rlnInstance,
decoder: createDecoder(TestContentTopic) decoder: createDecoder(TEST_CONSTANTS.contentTopic)
}); });
const bytes = await rlnEncoder.toWire({ payload }); const bytes = await rlnEncoder.toWire({ payload });
@ -67,78 +56,49 @@ describe("RLN codec with version 0", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined; expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
protoResult! protoResult!
))!; ))!;
expect(msg.rateLimitProof).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
it("toProtoObj", async function () { it("toProtoObj", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic }), createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance, rlnInstance,
index, index,
credential credential
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createDecoder(TestContentTopic) createDecoder(TEST_CONSTANTS.contentTopic)
); );
const proto = await rlnEncoder.toProtoObj({ payload }); const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined; expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
proto! proto!
)) as RlnMessage<DecodedMessage>; )) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
}); });
describe("RLN codec with version 1", () => { describe("RLN codec with version 1", () => {
it("Symmetric, toWire", async function () { it("Symmetric, toWire", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const symKey = generateSymmetricKey(); const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createSymEncoder({ createSymEncoder({
contentTopic: TestContentTopic, contentTopic: TEST_CONSTANTS.contentTopic,
symKey symKey
}), }),
rlnInstance, rlnInstance,
@ -147,45 +107,30 @@ describe("RLN codec with version 1", () => {
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createSymDecoder(TestContentTopic, symKey) createSymDecoder(TEST_CONSTANTS.contentTopic, symKey)
); );
const bytes = await rlnEncoder.toWire({ payload }); const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined; expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined; expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
protoResult! protoResult!
))!; ))!;
expect(msg.rateLimitProof).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
it("Symmetric, toProtoObj", async function () { it("Symmetric, toProtoObj", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const symKey = generateSymmetricKey(); const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createSymEncoder({ createSymEncoder({
contentTopic: TestContentTopic, contentTopic: TEST_CONSTANTS.contentTopic,
symKey symKey
}), }),
rlnInstance, rlnInstance,
@ -194,45 +139,29 @@ describe("RLN codec with version 1", () => {
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createSymDecoder(TestContentTopic, symKey) createSymDecoder(TEST_CONSTANTS.contentTopic, symKey)
); );
const proto = await rlnEncoder.toProtoObj({ payload }); const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined; expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
proto! proto!
)) as RlnMessage<DecodedMessage>; )) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
it("Asymmetric, toWire", async function () { it("Asymmetric, toWire", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const privateKey = generatePrivateKey(); const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey); const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createAsymEncoder({ createAsymEncoder({
contentTopic: TestContentTopic, contentTopic: TEST_CONSTANTS.contentTopic,
publicKey publicKey
}), }),
rlnInstance, rlnInstance,
@ -241,46 +170,31 @@ describe("RLN codec with version 1", () => {
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createAsymDecoder(TestContentTopic, privateKey) createAsymDecoder(TEST_CONSTANTS.contentTopic, privateKey)
); );
const bytes = await rlnEncoder.toWire({ payload }); const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined; expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined; expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
protoResult! protoResult!
))!; ))!;
expect(msg.rateLimitProof).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
it("Asymmetric, toProtoObj", async function () { it("Asymmetric, toProtoObj", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const privateKey = generatePrivateKey(); const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey); const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createAsymEncoder({ createAsymEncoder({
contentTopic: TestContentTopic, contentTopic: TEST_CONSTANTS.contentTopic,
publicKey publicKey
}), }),
rlnInstance, rlnInstance,
@ -289,106 +203,73 @@ describe("RLN codec with version 1", () => {
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createAsymDecoder(TestContentTopic, privateKey) createAsymDecoder(TEST_CONSTANTS.contentTopic, privateKey)
); );
const proto = await rlnEncoder.toProtoObj({ payload }); const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined; expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
proto! proto!
)) as RlnMessage<DecodedMessage>; )) as RlnMessage<DecodedMessage>;
expect(msg).to.not.be.undefined; verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(1);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
}); });
describe("RLN Codec - epoch", () => { describe("RLN Codec - epoch", () => {
it("toProtoObj", async function () { it("toProtoObj", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0;
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic }), createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic }),
rlnInstance, rlnInstance,
index, index,
credential credential
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createDecoder(TestContentTopic) createDecoder(TEST_CONSTANTS.contentTopic)
); );
const proto = await rlnEncoder.toProtoObj({ payload }); const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined; expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
proto! proto!
)) as RlnMessage<DecodedMessage>; )) as RlnMessage<DecodedMessage>;
const epochBytes = proto!.rateLimitProof!.epoch; const epochBytes = proto!.rateLimitProof!.epoch;
const epoch = epochBytesToInt(epochBytes); const epoch = epochBytesToInt(epochBytes);
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch!.toString(10).length).to.eq(9); expect(msg.epoch!.toString(10).length).to.eq(9);
expect(msg.epoch).to.eq(epoch); expect(msg.epoch).to.eq(epoch);
expect(msg.contentTopic).to.eq(TestContentTopic); verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
}); });
describe("RLN codec with version 0 and meta setter", () => { describe("RLN codec with version 0 and meta setter", () => {
// Encode the length of the payload
// Not a relevant real life example
const metaSetter = (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
it("toWire", async function () { it("toWire", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0; const metaSetter = createTestMetaSetter();
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = createRLNEncoder({ const rlnEncoder = createRLNEncoder({
encoder: createEncoder({ contentTopic: TestContentTopic, metaSetter }), encoder: createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
metaSetter
}),
rlnInstance, rlnInstance,
index, index,
credential credential
}); });
const rlnDecoder = createRLNDecoder({ const rlnDecoder = createRLNDecoder({
rlnInstance, rlnInstance,
decoder: createDecoder(TestContentTopic) decoder: createDecoder(TEST_CONSTANTS.contentTopic)
}); });
const bytes = await rlnEncoder.toWire({ payload }); const bytes = await rlnEncoder.toWire({ payload });
@ -397,7 +278,7 @@ describe("RLN codec with version 0 and meta setter", () => {
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!); const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined; expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
protoResult! protoResult!
))!; ))!;
@ -407,43 +288,30 @@ describe("RLN codec with version 0 and meta setter", () => {
}); });
expect(msg!.meta).to.deep.eq(expectedMeta); expect(msg!.meta).to.deep.eq(expectedMeta);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
it("toProtoObj", async function () { it("toProtoObj", async function () {
const rlnInstance = await createRLN(); const { rlnInstance, credential, index, payload } =
const credential = rlnInstance.zerokit.generateIdentityCredentials(); await createTestRLNCodecSetup();
const index = 0; const metaSetter = createTestMetaSetter();
const payload = new Uint8Array([1, 2, 3, 4, 5]);
rlnInstance.zerokit.insertMember(credential.IDCommitment);
const rlnEncoder = new RLNEncoder( const rlnEncoder = new RLNEncoder(
createEncoder({ contentTopic: TestContentTopic, metaSetter }), createEncoder({ contentTopic: TEST_CONSTANTS.contentTopic, metaSetter }),
rlnInstance, rlnInstance,
index, index,
credential credential
); );
const rlnDecoder = new RLNDecoder( const rlnDecoder = new RLNDecoder(
rlnInstance, rlnInstance,
createDecoder(TestContentTopic) createDecoder(TEST_CONSTANTS.contentTopic)
); );
const proto = await rlnEncoder.toProtoObj({ payload }); const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined; expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj( const msg = (await rlnDecoder.fromProtoObj(
EMPTY_PUBSUB_TOPIC, TEST_CONSTANTS.emptyPubsubTopic,
proto! proto!
)) as RlnMessage<DecodedMessage>; )) as RlnMessage<DecodedMessage>;
@ -453,18 +321,6 @@ describe("RLN codec with version 0 and meta setter", () => {
}); });
expect(msg!.meta).to.deep.eq(expectedMeta); expect(msg!.meta).to.deep.eq(expectedMeta);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
expect(msg).to.not.be.undefined;
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(TestContentTopic);
expect(msg.msg.version).to.eq(0);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}); });
}); });

View File

@ -0,0 +1,80 @@
import type { IProtoMessage } from "@waku/interfaces";
import { expect } from "chai";
import { createRLN } from "./create.js";
import type { IdentityCredential } from "./identity.js";
export interface TestRLNCodecSetup {
rlnInstance: any;
credential: IdentityCredential;
index: number;
payload: Uint8Array;
}
export const TEST_CONSTANTS = {
contentTopic: "/test/1/waku-message/utf8",
emptyPubsubTopic: "",
defaultIndex: 0,
defaultPayload: new Uint8Array([1, 2, 3, 4, 5])
} as const;
export const EMPTY_PROTO_MESSAGE = {
timestamp: undefined,
contentTopic: "",
ephemeral: undefined,
meta: undefined,
rateLimitProof: undefined,
version: undefined
} as const;
/**
* Creates a basic RLN setup for codec tests
*/
export async function createTestRLNCodecSetup(): Promise<TestRLNCodecSetup> {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
rlnInstance.zerokit.insertMember(credential.IDCommitment);
return {
rlnInstance,
credential,
index: TEST_CONSTANTS.defaultIndex,
payload: TEST_CONSTANTS.defaultPayload
};
}
/**
* Creates a meta setter function for testing
*/
export function createTestMetaSetter(): (
msg: IProtoMessage & { meta: undefined }
) => Uint8Array {
return (msg: IProtoMessage & { meta: undefined }): Uint8Array => {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, msg.payload.length, false);
return new Uint8Array(buffer);
};
}
/**
* Verifies common RLN message properties
*/
export function verifyRLNMessage(
msg: any,
payload: Uint8Array,
contentTopic: string,
version: number,
rlnInstance: any
): void {
expect(msg.rateLimitProof).to.not.be.undefined;
expect(msg.verify([rlnInstance.zerokit.getMerkleRoot()])).to.be.true;
expect(msg.verifyNoRoot()).to.be.true;
expect(msg.epoch).to.not.be.undefined;
expect(msg.epoch).to.be.gt(0);
expect(msg.contentTopic).to.eq(contentTopic);
expect(msg.msg.version).to.eq(version);
expect(msg.payload).to.deep.eq(payload);
expect(msg.timestamp).to.not.be.undefined;
}

View File

@ -1,392 +1,646 @@
export const RLN_ABI = [ export const RLN_ABI = [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{ {
type: "constructor", inputs: [
inputs: [], { internalType: "uint256", name: "idCommitment", type: "uint256" }
stateMutability: "nonpayable" ],
name: "CannotEraseActiveMembership",
type: "error"
},
{ inputs: [], name: "CannotExceedMaxTotalRateLimit", type: "error" },
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "CannotExtendNonGracePeriodMembership",
type: "error"
}, },
{ {
type: "error", inputs: [
name: "DuplicateIdCommitment", { internalType: "uint256", name: "idCommitment", type: "uint256" }
inputs: [] ],
},
{
type: "error",
name: "InvalidIdCommitment", name: "InvalidIdCommitment",
inputs: [ type: "error"
{
name: "idCommitment",
type: "uint256"
}
]
}, },
{ inputs: [], name: "InvalidMembershipRateLimit", type: "error" },
{ {
type: "error", inputs: [
{ internalType: "uint256", name: "startIndex", type: "uint256" },
{ internalType: "uint256", name: "endIndex", type: "uint256" }
],
name: "InvalidPaginationQuery", name: "InvalidPaginationQuery",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "MembershipDoesNotExist",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotEraseGracePeriodMembership",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotExtend",
type: "error"
},
{
anonymous: false,
inputs: [ inputs: [
{ {
name: "startIndex", indexed: false,
type: "uint256" internalType: "address",
}, name: "previousAdmin",
{
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" type: "address"
}, },
{ {
name: "token", indexed: false,
internalType: "address",
name: "newAdmin",
type: "address" type: "address"
} }
], ],
outputs: [ name: "AdminChanged",
{ type: "event"
name: "balance",
type: "uint256"
}
],
stateMutability: "view"
}, },
{ {
type: "function", anonymous: false,
name: "eraseMemberships",
inputs: [ inputs: [
{ {
name: "idCommitments", indexed: true,
type: "uint256[]" internalType: "address",
name: "beacon",
type: "address"
} }
], ],
outputs: [], name: "BeaconUpgraded",
stateMutability: "nonpayable" type: "event"
}, },
{ {
type: "function", anonymous: false,
name: "eraseMemberships",
inputs: [ inputs: [
{ { indexed: false, internalType: "uint8", name: "version", type: "uint8" }
name: "idCommitments",
type: "uint256[]"
},
{
name: "eraseFromMembershipSet",
type: "bool"
}
], ],
outputs: [], name: "Initialized",
stateMutability: "nonpayable" type: "event"
}, },
{ {
type: "function", anonymous: false,
name: "extendMemberships",
inputs: [
{
name: "idCommitments",
type: "uint256[]"
}
],
outputs: [],
stateMutability: "nonpayable"
},
{
type: "function",
name: "getMembershipInfo",
inputs: [ inputs: [
{ {
indexed: false,
internalType: "uint256",
name: "idCommitment", name: "idCommitment",
type: "uint256" type: "uint256"
} },
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
], ],
outputs: [ name: "MembershipErased",
type: "event"
},
{
anonymous: false,
inputs: [
{ {
name: "", indexed: false,
type: "uint32" internalType: "uint256",
name: "idCommitment",
type: "uint256"
}, },
{ {
name: "", indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32" type: "uint32"
}, },
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
name: "MembershipExpired",
type: "event"
},
{
anonymous: false,
inputs: [
{ {
name: "", indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" },
{
indexed: false,
internalType: "uint256",
name: "newGracePeriodStartTimestamp",
type: "uint256" type: "uint256"
} }
], ],
stateMutability: "view" name: "MembershipExtended",
type: "event"
}, },
{ {
type: "function", anonymous: false,
name: "getMerkleProof",
inputs: [ inputs: [
{ {
name: "index", indexed: false,
type: "uint40" internalType: "uint256",
} name: "idCommitment",
], type: "uint256"
outputs: [
{
name: "",
type: "uint256[20]"
}
],
stateMutability: "view"
},
{
type: "function",
name: "getRateCommitmentsInRangeBoundsInclusive",
inputs: [
{
name: "startIndex",
type: "uint32"
}, },
{ {
name: "endIndex", indexed: false,
type: "uint32" internalType: "uint256",
} name: "membershipRateLimit",
type: "uint256"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
], ],
outputs: [ name: "MembershipRegistered",
{ type: "event"
name: "",
type: "uint256[]"
}
],
stateMutability: "view"
}, },
{ {
type: "function", anonymous: false,
name: "gracePeriodDurationForNewMemberships",
inputs: [],
outputs: [
{
name: "",
type: "uint32"
}
],
stateMutability: "view"
},
{
type: "function",
name: "initialize",
inputs: [ inputs: [
{ {
name: "_priceCalculator", indexed: true,
internalType: "address",
name: "previousOwner",
type: "address" type: "address"
}, },
{ {
name: "_maxTotalRateLimit", indexed: true,
type: "uint32" internalType: "address",
}, name: "newOwner",
type: "address"
}
],
name: "OwnershipTransferred",
type: "event"
},
{
anonymous: false,
inputs: [
{ {
indexed: true,
internalType: "address",
name: "implementation",
type: "address"
}
],
name: "Upgraded",
type: "event"
},
{
inputs: [],
name: "MAX_MEMBERSHIP_SET_SIZE",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "MERKLE_TREE_DEPTH",
outputs: [{ internalType: "uint8", name: "", type: "uint8" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "Q",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "activeDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "currentTotalRateLimit",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "deployedBlockNumber",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
],
name: "depositsToWithdraw",
outputs: [{ internalType: "uint256", name: "balance", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" },
{ internalType: "bool", name: "eraseFromMembershipSet", type: "bool" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "extendMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "getMembershipInfo",
outputs: [
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint256", name: "", type: "uint256" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint40", name: "index", type: "uint40" }],
name: "getMerkleProof",
outputs: [{ internalType: "uint256[20]", name: "", type: "uint256[20]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "startIndex", type: "uint32" },
{ internalType: "uint32", name: "endIndex", type: "uint32" }
],
name: "getRateCommitmentsInRangeBoundsInclusive",
outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "gracePeriodDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint256", name: "", type: "uint256" }],
name: "indicesOfLazilyErasedMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" },
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" },
{
internalType: "uint32",
name: "_minMembershipRateLimit", name: "_minMembershipRateLimit",
type: "uint32" type: "uint32"
}, },
{ {
internalType: "uint32",
name: "_maxMembershipRateLimit", name: "_maxMembershipRateLimit",
type: "uint32" type: "uint32"
}, },
{ { internalType: "uint32", name: "_activeDuration", type: "uint32" },
name: "_activeDuration", { internalType: "uint32", name: "_gracePeriod", type: "uint32" }
type: "uint32"
},
{
name: "_gracePeriod",
type: "uint32"
}
], ],
name: "initialize",
outputs: [], outputs: [],
stateMutability: "nonpayable" stateMutability: "nonpayable",
type: "function"
}, },
{ {
type: "function", inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "isExpired", name: "isExpired",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [ inputs: [
{ { internalType: "uint256", name: "_idCommitment", type: "uint256" }
name: "_idCommitment",
type: "uint256"
}
], ],
name: "isInGracePeriod",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isInMembershipSet",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isValidIdCommitment",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "pure",
type: "function"
},
{
inputs: [{ internalType: "uint32", name: "rateLimit", type: "uint32" }],
name: "isValidMembershipRateLimit",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxTotalRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "membershipExpirationTimestamp",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "memberships",
outputs: [ outputs: [
{ internalType: "uint256", name: "depositAmount", type: "uint256" },
{ internalType: "uint32", name: "activeDuration", type: "uint32" },
{ {
name: "", internalType: "uint256",
type: "bool" name: "gracePeriodStartTimestamp",
} type: "uint256"
},
{ internalType: "uint32", name: "gracePeriodDuration", type: "uint32" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{ internalType: "uint32", name: "index", type: "uint32" },
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
], ],
stateMutability: "view" stateMutability: "view",
type: "function"
}, },
{ {
type: "function", inputs: [],
name: "merkleTree",
outputs: [
{ internalType: "uint40", name: "maxIndex", type: "uint40" },
{ internalType: "uint40", name: "numberOfLeaves", type: "uint40" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "minMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "nextFreeIndex",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "owner",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "priceCalculator",
outputs: [
{ internalType: "contract IPriceCalculator", name: "", type: "address" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "proxiableUUID",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{
internalType: "uint256[]",
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
name: "register", name: "register",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [ inputs: [
{ internalType: "address", name: "owner", type: "address" },
{ internalType: "uint256", name: "deadline", type: "uint256" },
{ internalType: "uint8", name: "v", type: "uint8" },
{ internalType: "bytes32", name: "r", type: "bytes32" },
{ internalType: "bytes32", name: "s", type: "bytes32" },
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{ {
name: "idCommitment", internalType: "uint256[]",
type: "uint256"
},
{
name: "rateLimit",
type: "uint32"
},
{
name: "idCommitmentsToErase", name: "idCommitmentsToErase",
type: "uint256[]" type: "uint256[]"
} }
], ],
outputs: [],
stateMutability: "nonpayable"
},
{
type: "function",
name: "registerWithPermit", 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: [], outputs: [],
stateMutability: "nonpayable" stateMutability: "nonpayable",
type: "function"
}, },
{ {
type: "event", inputs: [],
name: "MembershipRegistered", name: "renounceOwnership",
inputs: [ outputs: [],
{ stateMutability: "nonpayable",
name: "idCommitment", type: "function"
type: "uint256", },
indexed: false {
}, inputs: [],
{ name: "root",
name: "rateLimit", outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
type: "uint32", stateMutability: "view",
indexed: false type: "function"
},
{
name: "index",
type: "uint256",
indexed: false
}
],
anonymous: false
}, },
{ {
type: "event",
name: "MembershipRemoved",
inputs: [ inputs: [
{ {
name: "idCommitment", internalType: "uint32",
type: "uint256", name: "_activeDurationForNewMembership",
indexed: false type: "uint32"
},
{
name: "index",
type: "uint256",
indexed: false
} }
], ],
anonymous: false name: "setActiveDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_gracePeriodDurationForNewMembership",
type: "uint32"
}
],
name: "setGracePeriodDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_maxMembershipRateLimit",
type: "uint32"
}
],
name: "setMaxMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" }
],
name: "setMaxTotalRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_minMembershipRateLimit",
type: "uint32"
}
],
name: "setMinMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" }
],
name: "setPriceCalculator",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "newOwner", type: "address" }],
name: "transferOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" }
],
name: "upgradeTo",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" },
{ internalType: "bytes", name: "data", type: "bytes" }
],
name: "upgradeToAndCall",
outputs: [],
stateMutability: "payable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "token", type: "address" }],
name: "withdraw",
outputs: [],
stateMutability: "nonpayable",
type: "function"
} }
]; ];

View File

@ -1,8 +1,8 @@
import { RLN_ABI } from "./abi.js"; import { RLN_ABI } from "./abi.js";
export const SEPOLIA_CONTRACT = { export const LINEA_CONTRACT = {
chainId: 11155111, chainId: 59141,
address: "0xCB33Aa5B38d79E3D9Fa8B10afF38AA201399a7e3", address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6",
abi: RLN_ABI abi: RLN_ABI
}; };

View File

@ -4,244 +4,87 @@ import chaiAsPromised from "chai-as-promised";
import * as ethers from "ethers"; import * as ethers from "ethers";
import sinon, { SinonSandbox } from "sinon"; import sinon, { SinonSandbox } from "sinon";
import { createRLN } from "../create.js"; import { createTestRLNInstance, initializeRLNContract } from "./test-setup.js";
import type { IdentityCredential } from "../identity.js"; import {
createMockRegistryContract,
import { DEFAULT_RATE_LIMIT, SEPOLIA_CONTRACT } from "./constants.js"; createRegisterStub,
import { RLNContract } from "./rln_contract.js"; mockRLNRegisteredEvent,
verifyRegistration
} from "./test-utils.js";
use(chaiAsPromised); use(chaiAsPromised);
describe("RLN Contract abstraction - RLN", () => { describe("RLN Contract abstraction - RLN", () => {
let sandbox: SinonSandbox; let sandbox: SinonSandbox;
let rlnInstance: any;
let mockedRegistryContract: any;
const mockRateLimits = {
minRate: 20,
maxRate: 600,
maxTotalRate: 1000,
currentTotalRate: 500
};
beforeEach(async () => { beforeEach(async () => {
sandbox = sinon.createSandbox(); 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 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(() => { afterEach(() => {
sandbox.restore(); sandbox.restore();
}); });
it("should fetch members from events and store them in the RLN instance", async () => { describe("Member Registration", () => {
const rlnInstance = await createRLN(); it("should fetch members from events and store them in the RLN instance", async () => {
const { rlnInstance, insertMemberSpy } = await createTestRLNInstance();
const membershipRegisteredEvent = mockRLNRegisteredEvent();
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]);
const insertMemberSpy = sinon.stub(); const mockedRegistryContract = createMockRegistryContract({
rlnInstance.zerokit.insertMember = insertMemberSpy; queryFilter: queryFilterStub
});
const membershipRegisteredEvent = mockRLNRegisteredEvent(); const rlnContract = await initializeRLNContract(
rlnInstance,
mockedRegistryContract
);
const queryFilterStub = sinon.stub().returns([membershipRegisteredEvent]); await rlnContract.fetchMembers(rlnInstance, {
const mockedRegistryContract = { fromBlock: 0,
queryFilter: queryFilterStub, fetchRange: 1000,
provider: { fetchChunks: 2
getLogs: () => [], });
getBlockNumber: () => Promise.resolve(1000)
},
interface: {
getEvent: (eventName: string) => ({
name: eventName,
format: () => {}
})
},
filters: {
MembershipRegistered: () => ({}),
MembershipRemoved: () => ({})
},
on: () => ({}),
removeAllListeners: () => ({})
};
const provider = new ethers.providers.JsonRpcProvider(); expect(
const voidSigner = new ethers.VoidSigner( insertMemberSpy.calledWith(
SEPOLIA_CONTRACT.address, ethers.utils.zeroPad(
provider hexToBytes(membershipRegisteredEvent.args!.idCommitment),
); 32
const rlnContract = await RLNContract.init(rlnInstance, { )
address: SEPOLIA_CONTRACT.address,
signer: voidSigner,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract as unknown as ethers.Contract
});
await rlnContract.fetchMembers(rlnInstance, {
fromBlock: 0,
fetchRange: 1000,
fetchChunks: 2
});
expect(
insertMemberSpy.calledWith(
ethers.utils.zeroPad(
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
32
) )
) ).to.be.true;
).to.be.true; expect(queryFilterStub.called).to.be.true;
expect(queryFilterStub.called).to.be.true;
});
it("should register a member", async () => {
const mockSignature =
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
const rlnInstance = await createRLN();
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)
}
}
]
})
}); });
const mockedRegistryContract = { it("should register a member", async () => {
register: registerStub, const { rlnInstance, identity, insertMemberSpy } =
queryFilter: () => [membershipRegisteredEvent], await createTestRLNInstance();
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 provider = new ethers.providers.JsonRpcProvider(); const registerStub = createRegisterStub(identity);
const voidSigner = new ethers.VoidSigner( const mockedRegistryContract = createMockRegistryContract({
SEPOLIA_CONTRACT.address, register: registerStub,
provider queryFilter: () => []
); });
const rlnContract = await RLNContract.init(rlnInstance, {
signer: voidSigner, const rlnContract = await initializeRLNContract(
address: SEPOLIA_CONTRACT.address, rlnInstance,
rateLimit: DEFAULT_RATE_LIMIT, mockedRegistryContract
contract: mockedRegistryContract as unknown as ethers.Contract );
const decryptedCredentials =
await rlnContract.registerWithIdentity(identity);
if (!decryptedCredentials) {
throw new Error("Failed to retrieve credentials");
}
verifyRegistration(
decryptedCredentials,
identity,
registerStub,
insertMemberSpy
);
}); });
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 mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
return {
args: {
idCommitment:
idCommitment ||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
rateLimit: DEFAULT_RATE_LIMIT,
index: ethers.BigNumber.from(1)
},
event: "MembershipRegistered"
} as unknown as ethers.Event;
}

View File

@ -30,7 +30,7 @@ interface RLNContractInitOptions extends RLNContractOptions {
export interface MembershipRegisteredEvent { export interface MembershipRegisteredEvent {
idCommitment: string; idCommitment: string;
rateLimit: number; membershipRateLimit: ethers.BigNumber;
index: ethers.BigNumber; index: ethers.BigNumber;
} }
@ -65,7 +65,8 @@ export class RLNContract {
private _members: Map<number, Member> = new Map(); private _members: Map<number, Member> = new Map();
private _membersFilter: ethers.EventFilter; private _membersFilter: ethers.EventFilter;
private _membersRemovedFilter: ethers.EventFilter; private _membershipErasedFilter: ethers.EventFilter;
private _membersExpiredFilter: ethers.EventFilter;
/** /**
* Asynchronous initializer for RLNContract. * Asynchronous initializer for RLNContract.
@ -94,15 +95,7 @@ export class RLNContract {
contract contract
} = options; } = options;
if ( this.validateRateLimit(rateLimit);
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; this.rateLimit = rateLimit;
const initialRoot = rlnInstance.zerokit.getMerkleRoot(); const initialRoot = rlnInstance.zerokit.getMerkleRoot();
@ -111,9 +104,25 @@ export class RLNContract {
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer); this.contract = contract || new ethers.Contract(address, RLN_ABI, signer);
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot); this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
// Initialize event filters for MembershipRegistered and MembershipRemoved // Initialize event filters
this._membersFilter = this.contract.filters.MembershipRegistered(); this._membersFilter = this.contract.filters.MembershipRegistered();
this._membersRemovedFilter = this.contract.filters.MembershipRemoved(); this._membershipErasedFilter = this.contract.filters.MembershipErased();
this._membersExpiredFilter = this.contract.filters.MembershipExpired();
}
/**
* Validates that the rate limit is within the allowed range
* @throws Error if the rate limit is outside the allowed range
*/
private validateRateLimit(rateLimit: number): void {
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`
);
}
} }
/** /**
@ -182,7 +191,7 @@ export class RLNContract {
this.contract.maxTotalRateLimit(), this.contract.maxTotalRateLimit(),
this.contract.currentTotalRateLimit() this.contract.currentTotalRateLimit()
]); ]);
return maxTotal.sub(currentTotal).toNumber(); return Number(maxTotal) - Number(currentTotal);
} }
/** /**
@ -190,6 +199,7 @@ export class RLNContract {
* @param newRateLimit The new rate limit to use * @param newRateLimit The new rate limit to use
*/ */
public async setRateLimit(newRateLimit: number): Promise<void> { public async setRateLimit(newRateLimit: number): Promise<void> {
this.validateRateLimit(newRateLimit);
this.rateLimit = newRateLimit; this.rateLimit = newRateLimit;
} }
@ -207,11 +217,18 @@ export class RLNContract {
return this._membersFilter; return this._membersFilter;
} }
private get membersRemovedFilter(): ethers.EventFilter { private get membershipErasedFilter(): ethers.EventFilter {
if (!this._membersRemovedFilter) { if (!this._membershipErasedFilter) {
throw Error("MembersRemoved filter was not initialized."); throw Error("MembershipErased filter was not initialized.");
} }
return this._membersRemovedFilter; return this._membershipErasedFilter;
}
private get membersExpiredFilter(): ethers.EventFilter {
if (!this._membersExpiredFilter) {
throw Error("MembersExpired filter was not initialized.");
}
return this._membersExpiredFilter;
} }
public async fetchMembers( public async fetchMembers(
@ -226,10 +243,19 @@ export class RLNContract {
const removedMemberEvents = await queryFilter(this.contract, { const removedMemberEvents = await queryFilter(this.contract, {
fromBlock: this.deployBlock, fromBlock: this.deployBlock,
...options, ...options,
membersFilter: this.membersRemovedFilter membersFilter: this.membershipErasedFilter
});
const expiredMemberEvents = await queryFilter(this.contract, {
fromBlock: this.deployBlock,
...options,
membersFilter: this.membersExpiredFilter
}); });
const events = [...registeredMemberEvents, ...removedMemberEvents]; const events = [
...registeredMemberEvents,
...removedMemberEvents,
...expiredMemberEvents
];
this.processEvents(rlnInstance, events); this.processEvents(rlnInstance, events);
} }
@ -242,8 +268,26 @@ export class RLNContract {
return; return;
} }
if (evt.event === "MembershipRemoved") { if (
const index = evt.args.index as ethers.BigNumber; evt.event === "MembershipErased" ||
evt.event === "MembershipExpired"
) {
let index = evt.args.index;
if (!index) {
return;
}
if (typeof index === "number" || typeof index === "string") {
index = ethers.BigNumber.from(index);
} else {
log.error("Index is not a number or string", {
index,
event: evt
});
return;
}
const toRemoveVal = toRemoveTable.get(evt.blockNumber); const toRemoveVal = toRemoveTable.get(evt.blockNumber);
if (toRemoveVal != undefined) { if (toRemoveVal != undefined) {
toRemoveVal.push(index.toNumber()); toRemoveVal.push(index.toNumber());
@ -275,15 +319,21 @@ export class RLNContract {
if (!evt.args) return; if (!evt.args) return;
const _idCommitment = evt.args.idCommitment as string; const _idCommitment = evt.args.idCommitment as string;
const index = evt.args.index as ethers.BigNumber; let index = evt.args.index;
if (!_idCommitment || !index) { if (!_idCommitment || !index) {
return; return;
} }
if (typeof index === "number" || typeof index === "string") {
index = ethers.BigNumber.from(index);
}
const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32); const idCommitment = zeroPadLE(hexToBytes(_idCommitment), 32);
rlnInstance.zerokit.insertMember(idCommitment); rlnInstance.zerokit.insertMember(idCommitment);
this._members.set(index.toNumber(), {
const numericIndex = index.toNumber();
this._members.set(numericIndex, {
index, index,
idCommitment: _idCommitment idCommitment: _idCommitment
}); });
@ -316,7 +366,7 @@ export class RLNContract {
this.membersFilter, this.membersFilter,
( (
_idCommitment: string, _idCommitment: string,
_rateLimit: number, _membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber, _index: ethers.BigNumber,
event: ethers.Event event: ethers.Event
) => { ) => {
@ -325,9 +375,22 @@ export class RLNContract {
); );
this.contract.on( this.contract.on(
this.membersRemovedFilter, this.membershipErasedFilter,
( (
_idCommitment: string, _idCommitment: string,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber,
event: ethers.Event
) => {
this.processEvents(rlnInstance, [event]);
}
);
this.contract.on(
this.membersExpiredFilter,
(
_idCommitment: string,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber, _index: ethers.BigNumber,
event: ethers.Event event: ethers.Event
) => { ) => {
@ -344,15 +407,45 @@ export class RLNContract {
`Registering identity with rate limit: ${this.rateLimit} messages/epoch` `Registering identity with rate limit: ${this.rateLimit} messages/epoch`
); );
// Check if the ID commitment is already registered
const existingIndex = await this.getMemberIndex(
identity.IDCommitmentBigInt.toString()
);
if (existingIndex) {
throw new Error(
`ID commitment is already registered with index ${existingIndex}`
);
}
// Check if there's enough remaining rate limit
const remainingRateLimit = await this.getRemainingTotalRateLimit();
if (remainingRateLimit < this.rateLimit) {
throw new Error(
`Not enough remaining rate limit. Requested: ${this.rateLimit}, Available: ${remainingRateLimit}`
);
}
const estimatedGas = await this.contract.estimateGas.register(
identity.IDCommitmentBigInt,
this.rateLimit,
[]
);
const gasLimit = estimatedGas.add(10000);
const txRegisterResponse: ethers.ContractTransaction = const txRegisterResponse: ethers.ContractTransaction =
await this.contract.register( await this.contract.register(
identity.IDCommitmentBigInt, identity.IDCommitmentBigInt,
this.rateLimit, this.rateLimit,
[], [],
{ gasLimit: 300000 } { gasLimit }
); );
const txRegisterReceipt = await txRegisterResponse.wait(); const txRegisterReceipt = await txRegisterResponse.wait();
if (txRegisterReceipt.status === 0) {
throw new Error("Transaction failed on-chain");
}
const memberRegistered = txRegisterReceipt.events?.find( const memberRegistered = txRegisterReceipt.events?.find(
(event) => event.event === "MembershipRegistered" (event) => event.event === "MembershipRegistered"
); );
@ -366,30 +459,55 @@ export class RLNContract {
const decodedData: MembershipRegisteredEvent = { const decodedData: MembershipRegisteredEvent = {
idCommitment: memberRegistered.args.idCommitment, idCommitment: memberRegistered.args.idCommitment,
rateLimit: memberRegistered.args.rateLimit, membershipRateLimit: memberRegistered.args.membershipRateLimit,
index: memberRegistered.args.index index: memberRegistered.args.index
}; };
log.info( log.info(
`Successfully registered membership with index ${decodedData.index} ` + `Successfully registered membership with index ${decodedData.index} ` +
`and rate limit ${decodedData.rateLimit}` `and rate limit ${decodedData.membershipRateLimit}`
); );
const network = await this.contract.provider.getNetwork(); const network = await this.contract.provider.getNetwork();
const address = this.contract.address; const address = this.contract.address;
const membershipId = decodedData.index.toNumber(); const membershipId = Number(decodedData.index);
return { return {
identity, identity,
membership: { membership: {
address, address,
treeIndex: membershipId, treeIndex: membershipId,
chainId: network.chainId chainId: network.chainId,
rateLimit: decodedData.membershipRateLimit.toNumber()
} }
}; };
} catch (error) { } catch (error) {
log.error(`Error in registerWithIdentity: ${(error as Error).message}`); if (error instanceof Error) {
return undefined; const errorMessage = error.message;
log.error("registerWithIdentity - error message:", errorMessage);
log.error("registerWithIdentity - error stack:", error.stack);
// Try to extract more specific error information
if (errorMessage.includes("CannotExceedMaxTotalRateLimit")) {
throw new Error(
"Registration failed: Cannot exceed maximum total rate limit"
);
} else if (errorMessage.includes("InvalidIdCommitment")) {
throw new Error("Registration failed: Invalid ID commitment");
} else if (errorMessage.includes("InvalidMembershipRateLimit")) {
throw new Error("Registration failed: Invalid membership rate limit");
} else if (errorMessage.includes("execution reverted")) {
throw new Error(
"Contract execution reverted. Check contract requirements."
);
} else {
throw new Error(`Error in registerWithIdentity: ${errorMessage}`);
}
} else {
throw new Error("Unknown error in registerWithIdentity", {
cause: error
});
}
} }
} }
@ -467,25 +585,26 @@ export class RLNContract {
const decodedData: MembershipRegisteredEvent = { const decodedData: MembershipRegisteredEvent = {
idCommitment: memberRegistered.args.idCommitment, idCommitment: memberRegistered.args.idCommitment,
rateLimit: memberRegistered.args.rateLimit, membershipRateLimit: memberRegistered.args.membershipRateLimit,
index: memberRegistered.args.index index: memberRegistered.args.index
}; };
log.info( log.info(
`Successfully registered membership with permit. Index: ${decodedData.index}, ` + `Successfully registered membership with permit. Index: ${decodedData.index}, ` +
`Rate limit: ${decodedData.rateLimit}, Erased ${idCommitmentsToErase.length} commitments` `Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
); );
const network = await this.contract.provider.getNetwork(); const network = await this.contract.provider.getNetwork();
const address = this.contract.address; const address = this.contract.address;
const membershipId = decodedData.index.toNumber(); const membershipId = ethers.BigNumber.from(decodedData.index).toNumber();
return { return {
identity, identity,
membership: { membership: {
address, address,
treeIndex: membershipId, treeIndex: membershipId,
chainId: network.chainId chainId: network.chainId,
rateLimit: decodedData.membershipRateLimit.toNumber()
} }
}; };
} catch (error) { } catch (error) {
@ -560,16 +679,9 @@ export class RLNContract {
public async registerMembership( public async registerMembership(
idCommitment: string, idCommitment: string,
rateLimit: number = DEFAULT_RATE_LIMIT rateLimit: number = this.rateLimit
): Promise<ethers.ContractTransaction> { ): Promise<ethers.ContractTransaction> {
if ( this.validateRateLimit(rateLimit);
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, []); return this.contract.register(idCommitment, rateLimit, []);
} }

View File

@ -0,0 +1,86 @@
import { hexToBytes } from "@waku/utils/bytes";
import { ethers } from "ethers";
import sinon from "sinon";
import { createRLN } from "../create.js";
import type { IdentityCredential } from "../identity.js";
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
import { RLNContract } from "./rln_contract.js";
export interface TestRLNInstance {
rlnInstance: any;
identity: IdentityCredential;
insertMemberSpy: sinon.SinonStub;
}
/**
* Creates a test RLN instance with basic setup
*/
export async function createTestRLNInstance(): Promise<TestRLNInstance> {
const rlnInstance = await createRLN();
const insertMemberSpy = sinon.stub();
rlnInstance.zerokit.insertMember = insertMemberSpy;
const mockSignature =
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
const identity =
rlnInstance.zerokit.generateSeededIdentityCredential(mockSignature);
return {
rlnInstance,
identity,
insertMemberSpy
};
}
/**
* Initializes an RLN contract with the given registry contract
*/
export async function initializeRLNContract(
rlnInstance: any,
mockedRegistryContract: ethers.Contract
): Promise<RLNContract> {
const provider = new ethers.providers.JsonRpcProvider();
const voidSigner = new ethers.VoidSigner(LINEA_CONTRACT.address, provider);
const originalRegister = mockedRegistryContract.register;
(mockedRegistryContract as any).register = function (...args: any[]) {
const result = originalRegister.apply(this, args);
if (args[0] && rlnInstance.zerokit) {
const idCommitmentBigInt = args[0];
const idCommitmentHex =
"0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
const idCommitment = ethers.utils.zeroPad(
hexToBytes(idCommitmentHex),
32
);
rlnInstance.zerokit.insertMember(idCommitment);
}
return result;
};
const contract = await RLNContract.init(rlnInstance, {
address: LINEA_CONTRACT.address,
signer: voidSigner,
rateLimit: DEFAULT_RATE_LIMIT,
contract: mockedRegistryContract
});
return contract;
}
/**
* Common test message data
*/
export const TEST_DATA = {
contentTopic: "/test/1/waku-message/utf8",
emptyPubsubTopic: "",
testMessage: Uint8Array.from(
"Hello World".split("").map((x) => x.charCodeAt(0))
),
mockSignature:
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c"
} as const;

View File

@ -0,0 +1,179 @@
import { hexToBytes } from "@waku/utils/bytes";
import { expect } from "chai";
import * as ethers from "ethers";
import sinon from "sinon";
import type { IdentityCredential } from "../identity.js";
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
export const mockRateLimits = {
minRate: 20,
maxRate: 600,
maxTotalRate: 1200,
currentTotalRate: 500
};
type MockProvider = {
getLogs: () => never[];
getBlockNumber: () => Promise<number>;
getNetwork: () => Promise<{ chainId: number }>;
};
type MockFilters = {
MembershipRegistered: () => { address: string };
MembershipErased: () => { address: string };
MembershipExpired: () => { address: string };
};
export function createMockProvider(): MockProvider {
return {
getLogs: () => [],
getBlockNumber: () => Promise.resolve(1000),
getNetwork: () => Promise.resolve({ chainId: 11155111 })
};
}
export function createMockFilters(): MockFilters {
return {
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
};
}
type ContractOverrides = Partial<{
filters: Record<string, unknown>;
[key: string]: unknown;
}>;
export function createMockRegistryContract(
overrides: ContractOverrides = {}
): ethers.Contract {
const filters = {
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
};
const baseContract = {
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: () => [],
provider: createMockProvider(),
filters,
on: () => ({}),
removeAllListeners: () => ({}),
register: () => ({
wait: () =>
Promise.resolve({
events: [mockRLNRegisteredEvent()]
})
}),
estimateGas: {
register: () => Promise.resolve(ethers.BigNumber.from(100000))
},
functions: {
register: () => Promise.resolve()
},
getMemberIndex: () => Promise.resolve(null),
interface: {
getEvent: (eventName: string) => ({
name: eventName,
format: () => {}
})
},
address: LINEA_CONTRACT.address
};
// Merge overrides while preserving filters
const merged = {
...baseContract,
...overrides,
filters: { ...filters, ...(overrides.filters || {}) }
};
return merged as unknown as ethers.Contract;
}
export function mockRLNRegisteredEvent(idCommitment?: string): ethers.Event {
return {
args: {
idCommitment:
idCommitment ||
"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
index: ethers.BigNumber.from(1)
},
event: "MembershipRegistered"
} as unknown as ethers.Event;
}
export function formatIdCommitment(idCommitmentBigInt: bigint): string {
return "0x" + idCommitmentBigInt.toString(16).padStart(64, "0");
}
export function createRegisterStub(
identity: IdentityCredential
): sinon.SinonStub {
return sinon.stub().callsFake(() => ({
wait: () =>
Promise.resolve({
events: [
{
event: "MembershipRegistered",
args: {
idCommitment: formatIdCommitment(identity.IDCommitmentBigInt),
membershipRateLimit: ethers.BigNumber.from(DEFAULT_RATE_LIMIT),
index: ethers.BigNumber.from(1)
}
}
]
})
}));
}
export function verifyRegistration(
decryptedCredentials: any,
identity: IdentityCredential,
registerStub: sinon.SinonStub,
insertMemberSpy: sinon.SinonStub
): void {
if (!decryptedCredentials) {
throw new Error("Decrypted credentials should not be undefined");
}
// Verify registration call
expect(
registerStub.calledWith(
sinon.match.same(identity.IDCommitmentBigInt),
sinon.match.same(DEFAULT_RATE_LIMIT),
sinon.match.array,
sinon.match.object
)
).to.be.true;
// Verify credential properties
expect(decryptedCredentials).to.have.property("identity");
expect(decryptedCredentials).to.have.property("membership");
expect(decryptedCredentials.membership).to.include({
address: LINEA_CONTRACT.address,
treeIndex: 1
});
// Verify member insertion
const expectedIdCommitment = ethers.utils.zeroPad(
hexToBytes(formatIdCommitment(identity.IDCommitmentBigInt)),
32
);
expect(insertMemberSpy.callCount).to.equal(1);
expect(insertMemberSpy.getCall(0).args[0]).to.deep.equal(
expectedIdCommitment
);
}

View File

@ -1,6 +1,6 @@
import { RLNDecoder, RLNEncoder } from "./codec.js"; import { RLNDecoder, RLNEncoder } from "./codec.js";
import { RLN_ABI } from "./contract/abi.js"; import { RLN_ABI } from "./contract/abi.js";
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js"; import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
import { createRLN } from "./create.js"; import { createRLN } from "./create.js";
import { IdentityCredential } from "./identity.js"; import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js"; import { Keystore } from "./keystore/index.js";
@ -19,7 +19,18 @@ export {
RLNDecoder, RLNDecoder,
MerkleRootTracker, MerkleRootTracker,
RLNContract, RLNContract,
SEPOLIA_CONTRACT, LINEA_CONTRACT,
extractMetaMaskSigner, extractMetaMaskSigner,
RLN_ABI RLN_ABI
}; };
export type {
DecryptedCredentials,
EncryptedCredentials,
Keccak256Hash,
KeystoreEntity,
MembershipHash,
MembershipInfo,
Password,
Sha256Hash
} from "./keystore/types.js";

View File

@ -231,7 +231,8 @@ describe("Keystore", () => {
const membership = { const membership = {
chainId: "0xAA36A7", chainId: "0xAA36A7",
treeIndex: 8, treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71" address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
rateLimit: undefined
} as unknown as MembershipInfo; } as unknown as MembershipInfo;
const store = Keystore.create(); const store = Keystore.create();
@ -246,6 +247,11 @@ describe("Keystore", () => {
expectedHash, expectedHash,
DEFAULT_PASSWORD DEFAULT_PASSWORD
); );
if (!actualCredentials) {
throw new Error("Failed to retrieve credentials");
}
expect(actualCredentials).to.deep.equalInAnyOrder({ expect(actualCredentials).to.deep.equalInAnyOrder({
identity, identity,
membership membership
@ -276,7 +282,8 @@ describe("Keystore", () => {
const membership = { const membership = {
chainId: "0xAA36A7", chainId: "0xAA36A7",
treeIndex: 8, treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71" address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
rateLimit: undefined
} as unknown as MembershipInfo; } as unknown as MembershipInfo;
const store = Keystore.fromObject(NWAKU_KEYSTORE as any); const store = Keystore.fromObject(NWAKU_KEYSTORE as any);

View File

@ -274,7 +274,8 @@ export class Keystore {
membership: { membership: {
treeIndex: _.get(obj, "treeIndex"), treeIndex: _.get(obj, "treeIndex"),
chainId: _.get(obj, "membershipContract.chainId"), chainId: _.get(obj, "membershipContract.chainId"),
address: _.get(obj, "membershipContract.address") address: _.get(obj, "membershipContract.address"),
rateLimit: _.get(obj, "membershipContract.rateLimit")
} }
}; };
} catch (err) { } catch (err) {

View File

@ -11,6 +11,7 @@ export type MembershipInfo = {
chainId: number; chainId: number;
address: string; address: string;
treeIndex: number; treeIndex: number;
rateLimit: number;
}; };
export type KeystoreEntity = { export type KeystoreEntity = {

View File

@ -16,7 +16,7 @@ import {
type RLNEncoder type RLNEncoder
} from "./codec.js"; } from "./codec.js";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js"; import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { RLNContract, SEPOLIA_CONTRACT } from "./contract/index.js"; import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
import { IdentityCredential } from "./identity.js"; import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js"; import { Keystore } from "./keystore/index.js";
import type { import type {
@ -108,7 +108,7 @@ type StartRLNOptions = {
*/ */
signer?: ethers.Signer; signer?: ethers.Signer;
/** /**
* If not set - will use default SEPOLIA_CONTRACT address. * If not set - will use default LINEA_CONTRACT address.
*/ */
address?: string; address?: string;
/** /**
@ -190,10 +190,10 @@ export class RLNInstance {
const address = const address =
credentials?.membership.address || credentials?.membership.address ||
options.address || options.address ||
SEPOLIA_CONTRACT.address; LINEA_CONTRACT.address;
if (address === SEPOLIA_CONTRACT.address) { if (address === LINEA_CONTRACT.address) {
chainId = SEPOLIA_CONTRACT.chainId; chainId = LINEA_CONTRACT.chainId;
} }
const signer = options.signer || (await extractMetaMaskSigner()); const signer = options.signer || (await extractMetaMaskSigner());