merge with master

This commit is contained in:
Sasha 2025-10-07 00:03:56 +02:00
commit 62b43f061a
No known key found for this signature in database
48 changed files with 1136 additions and 2290 deletions

View File

@ -24,9 +24,11 @@
"cipherparams",
"ciphertext",
"circleci",
"circom",
"codecov",
"codegen",
"commitlint",
"cooldown",
"dependabot",
"dialable",
"dingpu",
@ -41,9 +43,7 @@
"Encrypters",
"enr",
"enrs",
"unsubscription",
"enrtree",
"unhandle",
"ephem",
"esnext",
"ethersproject",
@ -62,7 +62,6 @@
"ineed",
"IPAM",
"ipfs",
"cooldown",
"iwant",
"jdev",
"jswaku",
@ -122,9 +121,11 @@
"typedoc",
"undialable",
"unencrypted",
"unhandle",
"unmarshal",
"unmount",
"unmounts",
"unsubscription",
"untracked",
"upgrader",
"vacp",
@ -139,6 +140,7 @@
"weboko",
"websockets",
"wifi",
"WTNS",
"xsalsa20",
"zerokit",
"Привет",

8
package-lock.json generated
View File

@ -8414,9 +8414,9 @@
"link": true
},
"node_modules/@waku/zerokit-rln-wasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz",
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
"license": "MIT or Apache2"
},
"node_modules/@webassemblyjs/ast": {
@ -37468,7 +37468,7 @@
"@noble/hashes": "^1.2.0",
"@waku/core": "^0.0.39",
"@waku/utils": "^0.0.27",
"@waku/zerokit-rln-wasm": "^0.0.13",
"@waku/zerokit-rln-wasm": "^0.2.1",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",

View File

@ -69,7 +69,8 @@ test.describe("waku", () => {
console.log("Debug:", debug);
});
test("can dial peers", async ({ page }) => {
// TODO: https://github.com/waku-org/js-waku/issues/2619
test.skip("can dial peers", async ({ page }) => {
const result = await page.evaluate((peerAddrs) => {
return window.wakuAPI.dialPeers(window.waku, peerAddrs);
}, ACTIVE_PEERS);

View File

@ -9,7 +9,6 @@ import {
WakuEvent
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import { numberToBytes } from "@waku/utils/bytes";
import { Dialer } from "./dialer.js";
import { NetworkMonitor } from "./network_monitor.js";
@ -125,7 +124,6 @@ export class ConnectionLimiter implements IConnectionLimiter {
private async maintainConnections(): Promise<void> {
await this.maintainConnectionsCount();
await this.maintainBootstrapConnections();
await this.maintainTTLConnectedPeers();
}
private async onDisconnectedEvent(): Promise<void> {
@ -215,28 +213,6 @@ export class ConnectionLimiter implements IConnectionLimiter {
}
}
private async maintainTTLConnectedPeers(): Promise<void> {
log.info(`Maintaining TTL connected peers`);
const promises = this.libp2p.getConnections().map(async (c) => {
try {
await this.libp2p.peerStore.merge(c.remotePeer, {
metadata: {
ttl: numberToBytes(Date.now())
}
});
log.info(`TTL updated for connected peer ${c.remotePeer.toString()}`);
} catch (error) {
log.error(
`Unexpected error while maintaining TTL connected peer`,
error
);
}
});
await Promise.all(promises);
}
private async dialPeersFromStore(): Promise<void> {
log.info(`Dialing peers from store`);
@ -268,6 +244,9 @@ export class ConnectionLimiter implements IConnectionLimiter {
private async getPrioritizedPeers(): Promise<Peer[]> {
const allPeers = await this.libp2p.peerStore.all();
const allConnections = this.libp2p.getConnections();
const allConnectionsSet = new Set(
allConnections.map((c) => c.remotePeer.toString())
);
log.info(
`Found ${allPeers.length} peers in store, and found ${allConnections.length} connections`
@ -275,7 +254,7 @@ export class ConnectionLimiter implements IConnectionLimiter {
const notConnectedPeers = allPeers.filter(
(p) =>
!allConnections.some((c) => c.remotePeer.equals(p.id)) &&
!allConnectionsSet.has(p.id.toString()) &&
isAddressesSupported(
this.libp2p,
p.addresses.map((a) => a.multiaddr)

View File

@ -84,7 +84,7 @@ export interface SdsMessage {
senderId: string
messageId: string
channelId: string
lamportTimestamp?: number
lamportTimestamp?: bigint
causalHistory: HistoryEntry[]
bloomFilter?: Uint8Array
content?: Uint8Array
@ -117,7 +117,7 @@ export namespace SdsMessage {
if (obj.lamportTimestamp != null) {
w.uint32(80)
w.int32(obj.lamportTimestamp)
w.uint64(obj.lamportTimestamp)
}
if (obj.causalHistory != null) {
@ -167,7 +167,7 @@ export namespace SdsMessage {
break
}
case 10: {
obj.lamportTimestamp = reader.int32()
obj.lamportTimestamp = reader.uint64()
break
}
case 11: {

View File

@ -9,7 +9,7 @@ message SdsMessage {
string sender_id = 1; // Participant ID of the message sender
string message_id = 2; // Unique identifier of the message
string channel_id = 3; // Identifier of the channel to which the message belongs
optional int32 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel
optional uint64 lamport_timestamp = 10; // Logical timestamp for causal ordering in channel
repeated HistoryEntry causal_history = 11; // List of preceding message IDs that this message causally depends on. Generally 2 or 3 message IDs are included.
optional bytes bloom_filter = 12; // Bloom filter representing received message IDs in channel
optional bytes content = 20; // Actual content of the message

View File

@ -79,7 +79,7 @@
"@waku/core": "^0.0.39",
"@waku/utils": "^0.0.27",
"@noble/hashes": "^1.2.0",
"@waku/zerokit-rln-wasm": "^0.0.13",
"@waku/zerokit-rln-wasm": "^0.2.1",
"ethereum-cryptography": "^3.1.0",
"ethers": "^5.7.2",
"lodash": "^4.17.21",

View File

@ -1,363 +0,0 @@
import { createDecoder, createEncoder } from "@waku/core/lib/message/version_0";
import { IDecodedMessage } from "@waku/interfaces";
import {
generatePrivateKey,
generateSymmetricKey,
getPublicKey
} from "@waku/message-encryption";
import {
createDecoder as createAsymDecoder,
createEncoder as createAsymEncoder
} from "@waku/message-encryption/ecies";
import {
createDecoder as createSymDecoder,
createEncoder as createSymEncoder
} from "@waku/message-encryption/symmetric";
import { expect } from "chai";
import {
createRLNDecoder,
createRLNEncoder,
RLNDecoder,
RLNEncoder
} from "./codec.js";
import {
createTestMetaSetter,
createTestRLNCodecSetup,
EMPTY_PROTO_MESSAGE,
TEST_CONSTANTS,
verifyRLNMessage
} from "./codec.test-utils.js";
import { RlnMessage } from "./message.js";
import { epochBytesToInt } from "./utils/index.js";
describe("RLN codec with version 0", () => {
it("toWire", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = createRLNEncoder({
encoder: createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo
}),
rlnInstance,
index,
credential
});
const rlnDecoder = createRLNDecoder({
rlnInstance,
decoder: createDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo
)
});
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
it("toProtoObj", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = new RLNEncoder(
createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<IDecodedMessage>;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});
describe("RLN codec with version 1", () => {
it("Symmetric, toWire", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
createSymEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
symKey
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createSymDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo,
symKey
)
);
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Symmetric, toProtoObj", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const symKey = generateSymmetricKey();
const rlnEncoder = new RLNEncoder(
createSymEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
symKey
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createSymDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo,
symKey
)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
proto!
);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Asymmetric, toWire", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
createAsymEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
publicKey
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createAsymDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo,
privateKey
)
);
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
it("Asymmetric, toProtoObj", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const privateKey = generatePrivateKey();
const publicKey = getPublicKey(privateKey);
const rlnEncoder = new RLNEncoder(
createAsymEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
publicKey
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createAsymDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo,
privateKey
)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
proto!
);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 1, rlnInstance);
});
});
describe("RLN Codec - epoch", () => {
it("toProtoObj", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const rlnEncoder = new RLNEncoder(
createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<IDecodedMessage>;
const epochBytes = proto!.rateLimitProof!.epoch;
const epoch = epochBytesToInt(epochBytes);
expect(msg.epoch!.toString(10).length).to.eq(9);
expect(msg.epoch).to.eq(epoch);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});
describe("RLN codec with version 0 and meta setter", () => {
it("toWire", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const metaSetter = createTestMetaSetter();
const rlnEncoder = createRLNEncoder({
encoder: createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
metaSetter
}),
rlnInstance,
index,
credential
});
const rlnDecoder = createRLNDecoder({
rlnInstance,
decoder: createDecoder(
TEST_CONSTANTS.contentTopic,
TEST_CONSTANTS.routingInfo
)
});
const bytes = await rlnEncoder.toWire({ payload });
expect(bytes).to.not.be.undefined;
const protoResult = await rlnDecoder.fromWireToProtoObj(bytes!);
expect(protoResult).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
protoResult!
))!;
const expectedMeta = metaSetter({
...EMPTY_PROTO_MESSAGE,
payload: protoResult!.payload
});
expect(msg!.meta).to.deep.eq(expectedMeta);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
it("toProtoObj", async function () {
const { rlnInstance, credential, index, payload } =
await createTestRLNCodecSetup();
const metaSetter = createTestMetaSetter();
const rlnEncoder = new RLNEncoder(
createEncoder({
contentTopic: TEST_CONSTANTS.contentTopic,
routingInfo: TEST_CONSTANTS.routingInfo,
metaSetter
}),
rlnInstance,
index,
credential
);
const rlnDecoder = new RLNDecoder(
rlnInstance,
createDecoder(TEST_CONSTANTS.contentTopic, TEST_CONSTANTS.routingInfo)
);
const proto = await rlnEncoder.toProtoObj({ payload });
expect(proto).to.not.be.undefined;
const msg = (await rlnDecoder.fromProtoObj(
TEST_CONSTANTS.emptyPubsubTopic,
proto!
)) as RlnMessage<IDecodedMessage>;
const expectedMeta = metaSetter({
...EMPTY_PROTO_MESSAGE,
payload: msg!.payload
});
expect(msg!.meta).to.deep.eq(expectedMeta);
verifyRLNMessage(msg, payload, TEST_CONSTANTS.contentTopic, 0, rlnInstance);
});
});

View File

@ -1,88 +0,0 @@
import type { IProtoMessage } from "@waku/interfaces";
import { createRoutingInfo } from "@waku/utils";
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]),
routingInfo: createRoutingInfo(
{
clusterId: 0,
numShardsInCluster: 2
},
{ contentTopic: "/test/1/waku-message/utf8" }
)
} 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,138 +0,0 @@
import type {
IDecodedMessage,
IDecoder,
IEncoder,
IMessage,
IProtoMessage,
IRateLimitProof,
IRoutingInfo
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import type { IdentityCredential } from "./identity.js";
import { RlnMessage, toRLNSignal } from "./message.js";
import { RLNInstance } from "./rln.js";
const log = new Logger("rln:encoder");
export class RLNEncoder implements IEncoder {
private readonly idSecretHash: Uint8Array;
public constructor(
private readonly encoder: IEncoder,
private readonly rlnInstance: RLNInstance,
private readonly index: number,
identityCredential: IdentityCredential
) {
if (index < 0) throw new Error("Invalid membership index");
this.idSecretHash = identityCredential.IDSecretHash;
}
public async toWire(message: IMessage): Promise<Uint8Array | undefined> {
message.rateLimitProof = await this.generateProof(message);
log.info("Proof generated", message.rateLimitProof);
return this.encoder.toWire(message);
}
public async toProtoObj(
message: IMessage
): Promise<IProtoMessage | undefined> {
const protoMessage = await this.encoder.toProtoObj(message);
if (!protoMessage) return;
protoMessage.contentTopic = this.contentTopic;
protoMessage.rateLimitProof = await this.generateProof(message);
log.info("Proof generated", protoMessage.rateLimitProof);
return protoMessage;
}
private async generateProof(message: IMessage): Promise<IRateLimitProof> {
const signal = toRLNSignal(this.contentTopic, message);
return this.rlnInstance.zerokit.generateRLNProof(
signal,
this.index,
message.timestamp,
this.idSecretHash
);
}
public get pubsubTopic(): string {
return this.encoder.pubsubTopic;
}
public get routingInfo(): IRoutingInfo {
return this.encoder.routingInfo;
}
public get contentTopic(): string {
return this.encoder.contentTopic;
}
public get ephemeral(): boolean {
return this.encoder.ephemeral;
}
}
type RLNEncoderOptions = {
encoder: IEncoder;
rlnInstance: RLNInstance;
index: number;
credential: IdentityCredential;
};
export const createRLNEncoder = (options: RLNEncoderOptions): RLNEncoder => {
return new RLNEncoder(
options.encoder,
options.rlnInstance,
options.index,
options.credential
);
};
export class RLNDecoder<T extends IDecodedMessage>
implements IDecoder<RlnMessage<T>>
{
public constructor(
private readonly rlnInstance: RLNInstance,
private readonly decoder: IDecoder<T>
) {}
public get pubsubTopic(): string {
return this.decoder.pubsubTopic;
}
public get contentTopic(): string {
return this.decoder.contentTopic;
}
public fromWireToProtoObj(
bytes: Uint8Array
): Promise<IProtoMessage | undefined> {
const protoMessage = this.decoder.fromWireToProtoObj(bytes);
log.info("Message decoded", protoMessage);
return Promise.resolve(protoMessage);
}
public async fromProtoObj(
pubsubTopic: string,
proto: IProtoMessage
): Promise<RlnMessage<T> | undefined> {
const msg: T | undefined = await this.decoder.fromProtoObj(
pubsubTopic,
proto
);
if (!msg) return;
return new RlnMessage(this.rlnInstance, msg, proto.rateLimitProof);
}
}
type RLNDecoderOptions<T extends IDecodedMessage> = {
decoder: IDecoder<T>;
rlnInstance: RLNInstance;
};
export const createRLNDecoder = <T extends IDecodedMessage>(
options: RLNDecoderOptions<T>
): RLNDecoder<T> => {
return new RLNDecoder(options.rlnInstance, options.decoder);
};

View File

@ -19,26 +19,16 @@ export const PRICE_CALCULATOR_CONTRACT = {
* @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
STANDARD: 300,
MAX: 600
} 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)
MIN_RATE: RATE_LIMIT_TIERS.STANDARD,
MAX_RATE: RATE_LIMIT_TIERS.MAX,
MAX_TOTAL_RATE: 160_000,
EPOCH_LENGTH: 600
} as const;
/**
* Default Q value for the RLN contract
* This is the upper bound for the ID commitment
* @see https://github.com/waku-org/specs/blob/master/standards/core/rln-contract.md#implementation-suggestions
*/
export const RLN_Q = BigInt(
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
);
export const DEFAULT_RATE_LIMIT = RATE_LIMIT_PARAMS.MAX_RATE;

View File

@ -1,3 +1,2 @@
export { RLNContract } from "./rln_contract.js";
export * from "./constants.js";
export * from "./types.js";

View File

@ -3,7 +3,6 @@ import { ethers } from "ethers";
import { IdentityCredential } from "../identity.js";
import { DecryptedCredentials } from "../keystore/types.js";
import { BytesUtils } from "../utils/bytes.js";
import { RLN_ABI } from "./abi/rln.js";
import {
@ -632,7 +631,7 @@ export class RLNBaseContract {
permit.v,
permit.r,
permit.s,
BytesUtils.buildBigIntFromUint8ArrayBE(identity.IDCommitment),
identity.IDCommitmentBigInt,
this.rateLimit,
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
);

View File

@ -1,90 +0,0 @@
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 { createTestRLNInstance, initializeRLNContract } from "./test_setup.js";
import {
createMockRegistryContract,
createRegisterStub,
mockRLNRegisteredEvent,
verifyRegistration
} from "./test_utils.js";
use(chaiAsPromised);
describe("RLN Contract abstraction - RLN", () => {
let sandbox: SinonSandbox;
beforeEach(async () => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe("Member Registration", () => {
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 mockedRegistryContract = createMockRegistryContract({
queryFilter: queryFilterStub
});
const rlnContract = await initializeRLNContract(
rlnInstance,
mockedRegistryContract
);
await rlnContract.fetchMembers({
fromBlock: 0,
fetchRange: 1000,
fetchChunks: 2
});
expect(
insertMemberSpy.calledWith(
ethers.utils.zeroPad(
hexToBytes(membershipRegisteredEvent.args!.idCommitment),
32
)
)
).to.be.true;
expect(queryFilterStub.called).to.be.true;
});
it("should register a member", async () => {
const { rlnInstance, identity, insertMemberSpy } =
await createTestRLNInstance();
const registerStub = createRegisterStub(identity);
const mockedRegistryContract = createMockRegistryContract({
register: registerStub,
queryFilter: () => []
});
const rlnContract = await initializeRLNContract(
rlnInstance,
mockedRegistryContract
);
const decryptedCredentials =
await rlnContract.registerWithIdentity(identity);
if (!decryptedCredentials) {
throw new Error("Failed to retrieve credentials");
}
verifyRegistration(
decryptedCredentials,
identity,
registerStub,
insertMemberSpy
);
});
});
});

View File

@ -1,147 +0,0 @@
import { Logger } from "@waku/utils";
import { hexToBytes } from "@waku/utils/bytes";
import { ethers } from "ethers";
import type { RLNInstance } from "../rln.js";
import { MerkleRootTracker } from "../root_tracker.js";
import { BytesUtils } from "../utils/bytes.js";
import { RLNBaseContract } from "./rln_base_contract.js";
import { RLNContractInitOptions } from "./types.js";
const log = new Logger("rln:contract");
export class RLNContract extends RLNBaseContract {
private instance: RLNInstance;
private merkleRootTracker: MerkleRootTracker;
/**
* 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);
return rlnContract;
}
private constructor(
rlnInstance: RLNInstance,
options: RLNContractInitOptions
) {
super(options);
this.instance = rlnInstance;
const initialRoot = rlnInstance.zerokit.getMerkleRoot();
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
}
public override processEvents(events: ethers.Event[]): void {
const toRemoveTable = new Map<number, number[]>();
const toInsertTable = new Map<number, ethers.Event[]>();
events.forEach((evt) => {
if (!evt.args) {
return;
}
if (
evt.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);
if (toRemoveVal != undefined) {
toRemoveVal.push(index.toNumber());
toRemoveTable.set(evt.blockNumber, toRemoveVal);
} else {
toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
}
} else if (evt.event === "MembershipRegistered") {
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
if (eventsPerBlock == undefined) {
eventsPerBlock = [];
}
eventsPerBlock.push(evt);
toInsertTable.set(evt.blockNumber, eventsPerBlock);
}
});
this.removeMembers(this.instance, toRemoveTable);
this.insertMembers(this.instance, toInsertTable);
}
private insertMembers(
rlnInstance: RLNInstance,
toInsert: Map<number, ethers.Event[]>
): void {
toInsert.forEach((events: ethers.Event[], blockNumber: number) => {
events.forEach((evt) => {
if (!evt.args) return;
const _idCommitment = evt.args.idCommitment as string;
let index = evt.args.index;
if (!_idCommitment || !index) {
return;
}
if (typeof index === "number" || typeof index === "string") {
index = ethers.BigNumber.from(index);
}
const idCommitment = BytesUtils.zeroPadLE(
hexToBytes(_idCommitment),
32
);
rlnInstance.zerokit.insertMember(idCommitment);
const numericIndex = index.toNumber();
this._members.set(numericIndex, {
index,
idCommitment: _idCommitment
});
});
const currentRoot = rlnInstance.zerokit.getMerkleRoot();
this.merkleRootTracker.pushRoot(blockNumber, currentRoot);
});
}
private removeMembers(
rlnInstance: RLNInstance,
toRemove: Map<number, number[]>
): void {
const removeDescending = new Map([...toRemove].reverse());
removeDescending.forEach((indexes: number[], blockNumber: number) => {
indexes.forEach((index) => {
if (this._members.has(index)) {
this._members.delete(index);
rlnInstance.zerokit.deleteMember(index);
}
});
this.merkleRootTracker.backFill(blockNumber);
});
}
}

View File

@ -1,86 +0,0 @@
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, RLN_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(RLN_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: RLN_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"
};

View File

@ -1,179 +0,0 @@
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, RLN_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: RLN_CONTRACT.address }),
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
};
}
type ContractOverrides = Partial<{
filters: Record<string, unknown>;
[key: string]: unknown;
}>;
export function createMockRegistryContract(
overrides: ContractOverrides = {}
): ethers.Contract {
const filters = {
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
MembershipExpired: () => ({ address: RLN_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: RLN_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: RLN_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,137 +0,0 @@
import { assert, expect } from "chai";
import { createRLN } from "./create.js";
describe("js-rln", () => {
it("should verify a proof", async function () {
const rlnInstance = await createRLN();
const credential = rlnInstance.zerokit.generateIdentityCredentials();
//peer's index in the Merkle Tree
const index = 5;
// Create a Merkle tree with random members
for (let i = 0; i < 10; i++) {
if (i == index) {
// insert the current peer's pk
rlnInstance.zerokit.insertMember(credential.IDCommitment);
} else {
// create a new key pair
rlnInstance.zerokit.insertMember(
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
);
}
}
// prepare the message
const uint8Msg = Uint8Array.from(
"Hello World".split("").map((x) => x.charCodeAt(0))
);
// setting up the epoch
const epoch = new Date();
// generating proof
const proof = await rlnInstance.zerokit.generateRLNProof(
uint8Msg,
index,
epoch,
credential.IDSecretHash
);
try {
// verify the proof
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.true;
} catch (err) {
assert.fail(0, 1, "should not have failed proof verification");
}
try {
// Modifying the signal so it's invalid
uint8Msg[4] = 4;
// verify the proof
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.false;
} catch (err) {
console.log(err);
}
});
it("should verify a proof with a seeded membership key generation", async function () {
const rlnInstance = await createRLN();
const seed = "This is a test seed";
const credential =
rlnInstance.zerokit.generateSeededIdentityCredential(seed);
//peer's index in the Merkle Tree
const index = 5;
// Create a Merkle tree with random members
for (let i = 0; i < 10; i++) {
if (i == index) {
// insert the current peer's pk
rlnInstance.zerokit.insertMember(credential.IDCommitment);
} else {
// create a new key pair
rlnInstance.zerokit.insertMember(
rlnInstance.zerokit.generateIdentityCredentials().IDCommitment
);
}
}
// prepare the message
const uint8Msg = Uint8Array.from(
"Hello World".split("").map((x) => x.charCodeAt(0))
);
// setting up the epoch
const epoch = new Date();
// generating proof
const proof = await rlnInstance.zerokit.generateRLNProof(
uint8Msg,
index,
epoch,
credential.IDSecretHash
);
try {
// verify the proof
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.true;
} catch (err) {
assert.fail(0, 1, "should not have failed proof verification");
}
try {
// Modifying the signal so it's invalid
uint8Msg[4] = 4;
// verify the proof
const verifResult = rlnInstance.zerokit.verifyRLNProof(proof, uint8Msg);
expect(verifResult).to.be.false;
} catch (err) {
console.log(err);
}
});
it("should generate the same membership key if the same seed is provided", async function () {
const rlnInstance = await createRLN();
const seed = "This is a test seed";
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
memKeys1.IDCommitment.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDCommitment[index]);
});
memKeys1.IDNullifier.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDNullifier[index]);
});
memKeys1.IDSecretHash.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDSecretHash[index]);
});
memKeys1.IDTrapdoor.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
});
});
});

View File

@ -1,11 +1,8 @@
import { hmac } from "@noble/hashes/hmac";
import { sha256 } from "@noble/hashes/sha2";
import { Logger } from "@waku/utils";
import { ethers } from "ethers";
import { RLN_CONTRACT, RLN_Q } from "./contract/constants.js";
import { RLN_CONTRACT } from "./contract/constants.js";
import { RLNBaseContract } from "./contract/rln_base_contract.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
import type {
DecryptedCredentials,
@ -13,7 +10,6 @@ import type {
} from "./keystore/index.js";
import { KeystoreEntity, Password } from "./keystore/types.js";
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
import { BytesUtils } from "./utils/bytes.js";
import { extractMetaMaskSigner } from "./utils/index.js";
import { Zerokit } from "./zerokit.js";
@ -21,7 +17,6 @@ const log = new Logger("rln:credentials");
/**
* Manages credentials for RLN
* This is a lightweight implementation of the RLN contract that doesn't require Zerokit
* It is used to register membership and generate identity credentials
*/
export class RLNCredentialsManager {
@ -34,9 +29,9 @@ export class RLNCredentialsManager {
protected keystore = Keystore.create();
public credentials: undefined | DecryptedCredentials;
public zerokit: undefined | Zerokit;
public zerokit: Zerokit;
public constructor(zerokit?: Zerokit) {
public constructor(zerokit: Zerokit) {
log.info("RLNCredentialsManager initialized");
this.zerokit = zerokit;
}
@ -81,7 +76,7 @@ export class RLNCredentialsManager {
this.contract = await RLNBaseContract.create({
address: address!,
signer: signer!,
rateLimit: rateLimit ?? this.zerokit?.rateLimit
rateLimit: rateLimit ?? this.zerokit.rateLimit
});
log.info("RLNCredentialsManager successfully started");
@ -106,18 +101,10 @@ export class RLNCredentialsManager {
let identity = "identity" in options && options.identity;
if ("signature" in options) {
log.info("Generating identity from signature");
if (this.zerokit) {
log.info("Using Zerokit to generate identity");
identity = this.zerokit.generateSeededIdentityCredential(
options.signature
);
} else {
log.info("Using local implementation to generate identity");
identity = await this.generateSeededIdentityCredential(
options.signature
);
}
log.info("Using Zerokit to generate identity");
identity = this.zerokit.generateSeededIdentityCredential(
options.signature
);
}
if (!identity) {
@ -242,55 +229,4 @@ export class RLNCredentialsManager {
);
}
}
/**
* Generates an identity credential from a seed string
* This is a pure implementation that doesn't rely on Zerokit
* @param seed A string seed to generate the identity from
* @returns IdentityCredential
*/
private async generateSeededIdentityCredential(
seed: string
): Promise<IdentityCredential> {
log.info("Generating seeded identity credential");
// Convert the seed to bytes
const encoder = new TextEncoder();
const seedBytes = encoder.encode(seed);
// Generate deterministic values using HMAC-SHA256
// We use different context strings for each component to ensure they're different
const idTrapdoorBE = hmac(sha256, seedBytes, encoder.encode("IDTrapdoor"));
const idNullifierBE = hmac(
sha256,
seedBytes,
encoder.encode("IDNullifier")
);
const combinedBytes = new Uint8Array([...idTrapdoorBE, ...idNullifierBE]);
const idSecretHashBE = sha256(combinedBytes);
const idCommitmentRawBE = sha256(idSecretHashBE);
const idCommitmentBE = this.reduceIdCommitment(idCommitmentRawBE);
log.info(
"Successfully generated identity credential, storing in Big Endian format"
);
return new IdentityCredential(
idTrapdoorBE,
idNullifierBE,
idSecretHashBE,
idCommitmentBE
);
}
/**
* Helper: take 32-byte BE, reduce mod Q, return 32-byte BE
*/
private reduceIdCommitment(
bytesBE: Uint8Array,
limit: bigint = RLN_Q
): Uint8Array {
const nBE = BytesUtils.buildBigIntFromUint8ArrayBE(bytesBE);
return BytesUtils.bigIntToUint8Array32BE(nBE % limit);
}
}

View File

@ -11,8 +11,7 @@ export class IdentityCredential {
public readonly IDSecretHash: Uint8Array,
public readonly IDCommitment: Uint8Array
) {
this.IDCommitmentBigInt =
BytesUtils.buildBigIntFromUint8ArrayBE(IDCommitment);
this.IDCommitmentBigInt = BytesUtils.toBigInt(IDCommitment);
}
public static fromBytes(memKeys: Uint8Array): IdentityCredential {

View File

@ -1,28 +1,18 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import { RLN_ABI } from "./contract/abi/rln.js";
import { RLN_CONTRACT, RLNContract } from "./contract/index.js";
import { RLN_CONTRACT } from "./contract/index.js";
import { RLNBaseContract } from "./contract/rln_base_contract.js";
import { createRLN } from "./create.js";
import { RLNCredentialsManager } from "./credentials_manager.js";
import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js";
import { Proof } from "./proof.js";
import { RLNInstance } from "./rln.js";
import { MerkleRootTracker } from "./root_tracker.js";
import { extractMetaMaskSigner } from "./utils/index.js";
export {
RLNCredentialsManager,
RLNBaseContract,
createRLN,
Keystore,
RLNInstance,
IdentityCredential,
Proof,
RLNEncoder,
RLNDecoder,
MerkleRootTracker,
RLNContract,
RLN_CONTRACT,
extractMetaMaskSigner,
RLN_ABI

View File

@ -222,9 +222,7 @@ describe("Keystore", () => {
])
} as unknown as IdentityCredential;
// Add the missing property for test correctness
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
identity.IDCommitment
);
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,
@ -276,9 +274,7 @@ describe("Keystore", () => {
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15
]
} as unknown as IdentityCredential;
identity.IDCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(
identity.IDCommitment
);
identity.IDCommitmentBigInt = BytesUtils.toBigInt(identity.IDCommitment);
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,

View File

@ -264,20 +264,14 @@ export class Keystore {
_.get(obj, "identityCredential.idSecretHash", [])
);
// Big Endian
const idCommitmentBE = BytesUtils.switchEndianness(idCommitmentLE);
const idTrapdoorBE = BytesUtils.switchEndianness(idTrapdoorLE);
const idNullifierBE = BytesUtils.switchEndianness(idNullifierLE);
const idSecretHashBE = BytesUtils.switchEndianness(idSecretHashLE);
const idCommitmentBigInt =
BytesUtils.buildBigIntFromUint8ArrayBE(idCommitmentBE);
const idCommitmentBigInt = BytesUtils.toBigInt(idCommitmentLE);
return {
identity: {
IDCommitment: idCommitmentBE,
IDTrapdoor: idTrapdoorBE,
IDNullifier: idNullifierBE,
IDSecretHash: idSecretHashBE,
IDCommitment: idCommitmentLE,
IDTrapdoor: idTrapdoorLE,
IDNullifier: idNullifierLE,
IDSecretHash: idSecretHashLE,
IDCommitmentBigInt: idCommitmentBigInt
},
membership: {
@ -329,35 +323,18 @@ export class Keystore {
// follows nwaku implementation
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
// IdentityCredential is stored in Big Endian format => switch to Little Endian
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } =
options.identity;
const idCommitmentLE = BytesUtils.switchEndianness(IDCommitment);
const idNullifierLE = BytesUtils.switchEndianness(IDNullifier);
const idSecretHashLE = BytesUtils.switchEndianness(IDSecretHash);
const idTrapdoorLE = BytesUtils.switchEndianness(IDTrapdoor);
// eslint-disable-next-line no-console
console.log({
idCommitmentBE: IDCommitment,
idCommitmentLE,
idNullifierBE: IDNullifier,
idNullifierLE,
idSecretHashBE: IDSecretHash,
idSecretHashLE,
idTrapdoorBE: IDTrapdoor,
idTrapdoorLE
});
return utf8ToBytes(
JSON.stringify({
treeIndex: options.membership.treeIndex,
identityCredential: {
idCommitment: Array.from(idCommitmentLE),
idNullifier: Array.from(idNullifierLE),
idSecretHash: Array.from(idSecretHashLE),
idTrapdoor: Array.from(idTrapdoorLE)
idCommitment: Array.from(IDCommitment),
idNullifier: Array.from(IDNullifier),
idSecretHash: Array.from(IDSecretHash),
idTrapdoor: Array.from(IDTrapdoor)
},
membershipContract: {
chainId: options.membership.chainId,

View File

@ -1,81 +0,0 @@
import { message } from "@waku/core";
import type {
IDecodedMessage,
IMessage,
IRateLimitProof,
IRlnMessage
} from "@waku/interfaces";
import * as utils from "@waku/utils/bytes";
import { RLNInstance } from "./rln.js";
import { epochBytesToInt } from "./utils/index.js";
export function toRLNSignal(contentTopic: string, msg: IMessage): Uint8Array {
const contentTopicBytes = utils.utf8ToBytes(contentTopic ?? "");
return new Uint8Array([...(msg.payload ?? []), ...contentTopicBytes]);
}
export class RlnMessage<T extends IDecodedMessage> implements IRlnMessage {
public pubsubTopic = "";
public version = message.Version;
public constructor(
private rlnInstance: RLNInstance,
private msg: T,
public rateLimitProof: IRateLimitProof | undefined
) {}
public verify(roots: Uint8Array[]): boolean | undefined {
return this.rateLimitProof
? this.rlnInstance.zerokit.verifyWithRoots(
this.rateLimitProof,
toRLNSignal(this.msg.contentTopic, this.msg),
roots
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
: undefined;
}
public verifyNoRoot(): boolean | undefined {
return this.rateLimitProof
? this.rlnInstance.zerokit.verifyWithNoRoot(
this.rateLimitProof,
toRLNSignal(this.msg.contentTopic, this.msg)
) // this.rlnInstance.verifyRLNProof once issue status-im/nwaku#1248 is fixed
: undefined;
}
public get payload(): Uint8Array {
return this.msg.payload;
}
public get hash(): Uint8Array {
return this.msg.hash;
}
public get hashStr(): string {
return this.msg.hashStr;
}
public get contentTopic(): string {
return this.msg.contentTopic;
}
public get timestamp(): Date | undefined {
return this.msg.timestamp;
}
public get ephemeral(): boolean | undefined {
return this.msg.ephemeral;
}
public get meta(): Uint8Array | undefined {
return this.msg.meta;
}
public get epoch(): number | undefined {
const bytes = this.rateLimitProof?.epoch;
if (!bytes) return undefined;
return epochBytesToInt(bytes);
}
}

View File

@ -1,69 +0,0 @@
import type { IRateLimitProof } from "@waku/interfaces";
import { BytesUtils, poseidonHash } from "./utils/index.js";
const proofOffset = 128;
const rootOffset = proofOffset + 32;
const epochOffset = rootOffset + 32;
const shareXOffset = epochOffset + 32;
const shareYOffset = shareXOffset + 32;
const nullifierOffset = shareYOffset + 32;
const rlnIdentifierOffset = nullifierOffset + 32;
class ProofMetadata {
public constructor(
public readonly nullifier: Uint8Array,
public readonly shareX: Uint8Array,
public readonly shareY: Uint8Array,
public readonly externalNullifier: Uint8Array
) {}
}
export class Proof implements IRateLimitProof {
public readonly proof: Uint8Array;
public readonly merkleRoot: Uint8Array;
public readonly epoch: Uint8Array;
public readonly shareX: Uint8Array;
public readonly shareY: Uint8Array;
public readonly nullifier: Uint8Array;
public readonly rlnIdentifier: Uint8Array;
public constructor(proofBytes: Uint8Array) {
if (proofBytes.length < rlnIdentifierOffset) {
throw new Error("invalid proof");
}
// parse the proof as proof<128> | share_y<32> | nullifier<32> | root<32> | epoch<32> | share_x<32> | rln_identifier<32>
this.proof = proofBytes.subarray(0, proofOffset);
this.merkleRoot = proofBytes.subarray(proofOffset, rootOffset);
this.epoch = proofBytes.subarray(rootOffset, epochOffset);
this.shareX = proofBytes.subarray(epochOffset, shareXOffset);
this.shareY = proofBytes.subarray(shareXOffset, shareYOffset);
this.nullifier = proofBytes.subarray(shareYOffset, nullifierOffset);
this.rlnIdentifier = proofBytes.subarray(
nullifierOffset,
rlnIdentifierOffset
);
}
public extractMetadata(): ProofMetadata {
const externalNullifier = poseidonHash(this.epoch, this.rlnIdentifier);
return new ProofMetadata(
this.nullifier,
this.shareX,
this.shareY,
externalNullifier
);
}
}
export function proofToBytes(p: IRateLimitProof): Uint8Array {
return BytesUtils.concatenate(
p.proof,
p.merkleRoot,
p.epoch,
p.shareX,
p.shareY,
p.nullifier,
p.rlnIdentifier
);
}

Binary file not shown.

View File

@ -1,13 +0,0 @@
declare const verificationKey: {
protocol: string;
curve: string;
nPublic: number;
vk_alpha_1: string[];
vk_beta_2: string[][];
vk_gamma_2: string[][];
vk_delta_2: string[][];
vk_alphabeta_12: string[][][];
IC: string[][];
};
export default verificationKey;

View File

@ -1,112 +0,0 @@
const verificationKey = {
protocol: "groth16",
curve: "bn128",
nPublic: 6,
vk_alpha_1: [
"20124996762962216725442980738609010303800849578410091356605067053491763969391",
"9118593021526896828671519912099489027245924097793322973632351264852174143923",
"1"
],
vk_beta_2: [
[
"4693952934005375501364248788849686435240706020501681709396105298107971354382",
"14346958885444710485362620645446987998958218205939139994511461437152241966681"
],
[
"16851772916911573982706166384196538392731905827088356034885868448550849804972",
"823612331030938060799959717749043047845343400798220427319188951998582076532"
],
["1", "0"]
],
vk_gamma_2: [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
["1", "0"]
],
vk_delta_2: [
[
"8353516066399360694538747105302262515182301251524941126222712285088022964076",
"9329524012539638256356482961742014315122377605267454801030953882967973561832"
],
[
"16805391589556134376869247619848130874761233086443465978238468412168162326401",
"10111259694977636294287802909665108497237922060047080343914303287629927847739"
],
["1", "0"]
],
vk_alphabeta_12: [
[
[
"12608968655665301215455851857466367636344427685631271961542642719683786103711",
"9849575605876329747382930567422916152871921500826003490242628251047652318086"
],
[
"6322029441245076030714726551623552073612922718416871603535535085523083939021",
"8700115492541474338049149013125102281865518624059015445617546140629435818912"
],
[
"10674973475340072635573101639867487770811074181475255667220644196793546640210",
"2926286967251299230490668407790788696102889214647256022788211245826267484824"
]
],
[
[
"9660441540778523475944706619139394922744328902833875392144658911530830074820",
"19548113127774514328631808547691096362144426239827206966690021428110281506546"
],
[
"1870837942477655969123169532603615788122896469891695773961478956740992497097",
"12536105729661705698805725105036536744930776470051238187456307227425796690780"
],
[
"21811903352654147452884857281720047789720483752548991551595462057142824037334",
"19021616763967199151052893283384285352200445499680068407023236283004353578353"
]
]
],
IC: [
[
"11992897507809711711025355300535923222599547639134311050809253678876341466909",
"17181525095924075896332561978747020491074338784673526378866503154966799128110",
"1"
],
[
"17018665030246167677911144513385572506766200776123272044534328594850561667818",
"18601114175490465275436712413925513066546725461375425769709566180981674884464",
"1"
],
[
"18799470100699658367834559797874857804183288553462108031963980039244731716542",
"13064227487174191981628537974951887429496059857753101852163607049188825592007",
"1"
],
[
"17432501889058124609368103715904104425610382063762621017593209214189134571156",
"13406815149699834788256141097399354592751313348962590382887503595131085938635",
"1"
],
[
"10320964835612716439094703312987075811498239445882526576970512041988148264481",
"9024164961646353611176283204118089412001502110138072989569118393359029324867",
"1"
],
[
"718355081067365548229685160476620267257521491773976402837645005858953849298",
"14635482993933988261008156660773180150752190597753512086153001683711587601974",
"1"
],
[
"11777720285956632126519898515392071627539405001940313098390150593689568177535",
"8483603647274280691250972408211651407952870456587066148445913156086740744515",
"1"
]
]
};
export default verificationKey;

View File

@ -1,11 +1,25 @@
export async function builder(
export const builder: (
code: Uint8Array,
sanityCheck: boolean
): Promise<WitnessCalculator>;
sanityCheck?: boolean
) => Promise<WitnessCalculator>;
export class WitnessCalculator {
public calculateWitness(
input: unknown,
sanityCheck: boolean
): Promise<Array<bigint>>;
constructor(instance: any, sanityCheck?: boolean);
circom_version(): number;
calculateWitness(
input: Record<string, unknown>,
sanityCheck?: boolean
): Promise<bigint[]>;
calculateBinWitness(
input: Record<string, unknown>,
sanityCheck?: boolean
): Promise<Uint8Array>;
calculateWTNSBin(
input: Record<string, unknown>,
sanityCheck?: boolean
): Promise<Uint8Array>;
}

View File

@ -1,6 +1,6 @@
// File generated with https://github.com/iden3/circom
// following the instructions from:
// https://github.com/vacp2p/zerokit/tree/master/rln#compiling-circuits
// https://github.com/vacp2p/zerokit/tree/master/rln#advanced-custom-circuit-compilation
export async function builder(code, options) {
options = options || {};

View File

@ -1,37 +1,14 @@
import { createDecoder, createEncoder } from "@waku/core";
import type {
ContentTopic,
IDecodedMessage,
IRoutingInfo,
EncoderOptions as WakuEncoderOptions
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import init from "@waku/zerokit-rln-wasm";
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import init, * as zerokitRLN from "@waku/zerokit-rln-wasm";
import {
createRLNDecoder,
createRLNEncoder,
type RLNDecoder,
type RLNEncoder
} from "./codec.js";
import { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { RLNCredentialsManager } from "./credentials_manager.js";
import type {
DecryptedCredentials,
EncryptedCredentials
} from "./keystore/index.js";
import verificationKey from "./resources/verification_key";
import * as wc from "./resources/witness_calculator";
import { WitnessCalculator } from "./resources/witness_calculator";
import { Zerokit } from "./zerokit.js";
const log = new Logger("rln");
type WakuRLNEncoderOptions = WakuEncoderOptions & {
credentials: EncryptedCredentials | DecryptedCredentials;
};
export class RLNInstance extends RLNCredentialsManager {
/**
* Create an instance of RLN
@ -39,18 +16,13 @@ export class RLNInstance extends RLNCredentialsManager {
*/
public static async create(): Promise<RLNInstance> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (init as any)?.();
zerokitRLN.init_panic_hook();
await init();
zerokitRLN.initPanicHook();
const witnessCalculator = await RLNInstance.loadWitnessCalculator();
const zkey = await RLNInstance.loadZkey();
const stringEncoder = new TextEncoder();
const vkey = stringEncoder.encode(JSON.stringify(verificationKey));
const DEPTH = 20;
const zkRLN = zerokitRLN.newRLN(DEPTH, zkey, vkey);
const zkRLN = zerokitRLN.newRLN(zkey);
const zerokit = new Zerokit(zkRLN, witnessCalculator, DEFAULT_RATE_LIMIT);
return new RLNInstance(zerokit);
@ -64,39 +36,6 @@ export class RLNInstance extends RLNCredentialsManager {
super(zerokit);
}
public async createEncoder(
options: WakuRLNEncoderOptions
): Promise<RLNEncoder> {
const { credentials: decryptedCredentials } =
await RLNInstance.decryptCredentialsIfNeeded(options.credentials);
const credentials = decryptedCredentials || this.credentials;
if (!credentials) {
throw Error(
"Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly."
);
}
await this.verifyCredentialsAgainstContract(credentials);
return createRLNEncoder({
encoder: createEncoder(options),
rlnInstance: this,
index: credentials.membership.treeIndex,
credential: credentials.identity
});
}
public createDecoder(
contentTopic: ContentTopic,
routingInfo: IRoutingInfo
): RLNDecoder<IDecodedMessage> {
return createRLNDecoder({
rlnInstance: this,
decoder: createDecoder(contentTopic, routingInfo)
});
}
public static async loadWitnessCalculator(): Promise<WitnessCalculator> {
try {
const url = new URL("./resources/rln.wasm", import.meta.url);

View File

@ -1,56 +0,0 @@
import { assert, expect } from "chai";
import { MerkleRootTracker } from "./root_tracker.js";
describe("js-rln", () => {
it("should track merkle roots and backfill from block number", async function () {
const acceptableRootWindow = 3;
const tracker = new MerkleRootTracker(
acceptableRootWindow,
new Uint8Array([0, 0, 0, 0])
);
expect(tracker.roots()).to.have.length(1);
expect(tracker.buffer()).to.have.length(0);
expect(tracker.roots()[0]).to.deep.equal(new Uint8Array([0, 0, 0, 0]));
for (let i = 1; i <= 30; i++) {
tracker.pushRoot(i, new Uint8Array([0, 0, 0, i]));
}
expect(tracker.roots()).to.have.length(acceptableRootWindow);
expect(tracker.buffer()).to.have.length(20);
assert.sameDeepMembers(tracker.roots(), [
new Uint8Array([0, 0, 0, 30]),
new Uint8Array([0, 0, 0, 29]),
new Uint8Array([0, 0, 0, 28])
]);
// Buffer should keep track of 20 blocks previous to the current valid merkle root window
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
expect(tracker.buffer()[19]).to.be.eql(new Uint8Array([0, 0, 0, 27]));
// Remove roots 29 and 30
tracker.backFill(29);
assert.sameDeepMembers(tracker.roots(), [
new Uint8Array([0, 0, 0, 28]),
new Uint8Array([0, 0, 0, 27]),
new Uint8Array([0, 0, 0, 26])
]);
expect(tracker.buffer()).to.have.length(18);
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
expect(tracker.buffer()[17]).to.be.eql(new Uint8Array([0, 0, 0, 25]));
// Remove roots from block 15 onwards. These blocks exists within the buffer
tracker.backFill(15);
assert.sameDeepMembers(tracker.roots(), [
new Uint8Array([0, 0, 0, 14]),
new Uint8Array([0, 0, 0, 13]),
new Uint8Array([0, 0, 0, 12])
]);
expect(tracker.buffer()).to.have.length(4);
expect(tracker.buffer()[0]).to.be.eql(new Uint8Array([0, 0, 0, 8]));
expect(tracker.buffer()[3]).to.be.eql(new Uint8Array([0, 0, 0, 11]));
});
});

View File

@ -1,92 +0,0 @@
class RootPerBlock {
public constructor(
public root: Uint8Array,
public blockNumber: number
) {}
}
const maxBufferSize = 20;
export class MerkleRootTracker {
private validMerkleRoots: Array<RootPerBlock> = new Array<RootPerBlock>();
private merkleRootBuffer: Array<RootPerBlock> = new Array<RootPerBlock>();
public constructor(
private acceptableRootWindowSize: number,
initialRoot: Uint8Array
) {
this.pushRoot(0, initialRoot);
}
public backFill(fromBlockNumber: number): void {
if (this.validMerkleRoots.length == 0) return;
let numBlocks = 0;
for (let i = this.validMerkleRoots.length - 1; i >= 0; i--) {
if (this.validMerkleRoots[i].blockNumber >= fromBlockNumber) {
numBlocks++;
}
}
if (numBlocks == 0) return;
const olderBlock = fromBlockNumber < this.validMerkleRoots[0].blockNumber;
// Remove last roots
let rootsToPop = numBlocks;
if (this.validMerkleRoots.length < rootsToPop) {
rootsToPop = this.validMerkleRoots.length;
}
this.validMerkleRoots = this.validMerkleRoots.slice(
0,
this.validMerkleRoots.length - rootsToPop
);
if (this.merkleRootBuffer.length == 0) return;
if (olderBlock) {
const idx = this.merkleRootBuffer.findIndex(
(x) => x.blockNumber == fromBlockNumber
);
if (idx > -1) {
this.merkleRootBuffer = this.merkleRootBuffer.slice(0, idx);
}
}
// Backfill the tree's acceptable roots
let rootsToRestore =
this.acceptableRootWindowSize - this.validMerkleRoots.length;
if (this.merkleRootBuffer.length < rootsToRestore) {
rootsToRestore = this.merkleRootBuffer.length;
}
for (let i = 0; i < rootsToRestore; i++) {
const x = this.merkleRootBuffer.pop();
if (x) this.validMerkleRoots.unshift(x);
}
}
public pushRoot(blockNumber: number, root: Uint8Array): void {
this.validMerkleRoots.push(new RootPerBlock(root, blockNumber));
// Maintain valid merkle root window
if (this.validMerkleRoots.length > this.acceptableRootWindowSize) {
const x = this.validMerkleRoots.shift();
if (x) this.merkleRootBuffer.push(x);
}
// Maintain merkle root buffer
if (this.merkleRootBuffer.length > maxBufferSize) {
this.merkleRootBuffer.shift();
}
}
public roots(): Array<Uint8Array> {
return this.validMerkleRoots.map((x) => x.root);
}
public buffer(): Array<Uint8Array> {
return this.merkleRootBuffer.map((x) => x.root);
}
}

View File

@ -1,56 +1,52 @@
export class BytesUtils {
/**
* Switches endianness of a byte array
* Concatenate Uint8Arrays
* @param input
* @returns concatenation of all Uint8Array received as input
*/
public static switchEndianness(bytes: Uint8Array): Uint8Array {
return new Uint8Array([...bytes].reverse());
}
/**
* Builds a BigInt from a big-endian Uint8Array
* @param bytes The big-endian bytes to convert
* @returns The resulting BigInt in big-endian format
*/
public static buildBigIntFromUint8ArrayBE(bytes: Uint8Array): bigint {
let result = 0n;
for (let i = 0; i < bytes.length; i++) {
result = (result << 8n) + BigInt(bytes[i]);
public static concatenate(...input: Uint8Array[]): Uint8Array {
let totalLength = 0;
for (const arr of input) {
totalLength += arr.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of input) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
/**
* Switches endianness of a bigint value
* @param value The bigint value to switch endianness for
* @returns The bigint value with reversed endianness
* Convert a Uint8Array to a BigInt with configurable input endianness
* @param bytes - The byte array to convert
* @param inputEndianness - Endianness of the input bytes ('big' or 'little')
* @returns BigInt representation of the bytes
*/
public static switchEndiannessBigInt(value: bigint): bigint {
// Convert bigint to byte array
const bytes = [];
let tempValue = value;
while (tempValue > 0n) {
bytes.push(Number(tempValue & 0xffn));
tempValue >>= 8n;
public static toBigInt(
bytes: Uint8Array,
inputEndianness: "big" | "little" = "little"
): bigint {
if (bytes.length === 0) {
return 0n;
}
// Reverse bytes and convert back to bigint
return bytes
.reverse()
.reduce((acc, byte) => (acc << 8n) + BigInt(byte), 0n);
}
// Create a copy to avoid modifying the original array
const workingBytes = new Uint8Array(bytes);
/**
* Converts a big-endian bigint to a 32-byte big-endian Uint8Array
* @param value The big-endian bigint to convert
* @returns A 32-byte big-endian Uint8Array
*/
public static bigIntToUint8Array32BE(value: bigint): Uint8Array {
const bytes = new Uint8Array(32);
for (let i = 31; i >= 0; i--) {
bytes[i] = Number(value & 0xffn);
value >>= 8n;
// Reverse bytes if input is little-endian to work with big-endian internally
if (inputEndianness === "little") {
workingBytes.reverse();
}
return bytes;
// Convert to BigInt
let result = 0n;
for (let i = 0; i < workingBytes.length; i++) {
result = (result << 8n) | BigInt(workingBytes[i]);
}
return result;
}
/**
@ -81,20 +77,6 @@ export class BytesUtils {
return buf;
}
/**
* Fills with zeros to set length
* @param array little endian Uint8Array
* @param length amount to pad
* @returns little endian Uint8Array padded with zeros to set length
*/
public static zeroPadLE(array: Uint8Array, length: number): Uint8Array {
const result = new Uint8Array(length);
for (let i = 0; i < length; i++) {
result[i] = array[i] || 0;
}
return result;
}
// Adapted from https://github.com/feross/buffer
public static checkInt(
buf: Uint8Array,
@ -108,23 +90,4 @@ export class BytesUtils {
throw new RangeError('"value" argument is out of bounds');
if (offset + ext > buf.length) throw new RangeError("Index out of range");
}
/**
* Concatenate Uint8Arrays
* @param input
* @returns concatenation of all Uint8Array received as input
*/
public static concatenate(...input: Uint8Array[]): Uint8Array {
let totalLength = 0;
for (const arr of input) {
totalLength += arr.length;
}
const result = new Uint8Array(totalLength);
let offset = 0;
for (const arr of input) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
}

View File

@ -0,0 +1,26 @@
import { expect } from "chai";
import { RLNInstance } from "./rln.js";
describe("@waku/rln", () => {
it("should generate the same membership key if the same seed is provided", async function () {
const rlnInstance = await RLNInstance.create();
const seed = "This is a test seed";
const memKeys1 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
const memKeys2 = rlnInstance.zerokit.generateSeededIdentityCredential(seed);
memKeys1.IDCommitment.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDCommitment[index]);
});
memKeys1.IDNullifier.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDNullifier[index]);
});
memKeys1.IDSecretHash.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDSecretHash[index]);
});
memKeys1.IDTrapdoor.forEach((element, index) => {
expect(element).to.equal(memKeys2.IDTrapdoor[index]);
});
});
});

View File

@ -1,11 +1,8 @@
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 { DEFAULT_RATE_LIMIT } from "./contract/constants.js";
import { IdentityCredential } from "./identity.js";
import { Proof, proofToBytes } from "./proof.js";
import { WitnessCalculator } from "./resources/witness_calculator";
import { BytesUtils, dateToEpoch, epochIntToBytes } from "./utils/index.js";
export class Zerokit {
public constructor(
@ -26,226 +23,13 @@ export class Zerokit {
return this._rateLimit;
}
public generateIdentityCredentials(): IdentityCredential {
const memKeys = zerokitRLN.generateExtendedMembershipKey(this.zkRLN); // TODO: rename this function in zerokit rln-wasm
return IdentityCredential.fromBytes(memKeys);
}
public generateSeededIdentityCredential(seed: string): IdentityCredential {
const stringEncoder = new TextEncoder();
const seedBytes = stringEncoder.encode(seed);
// TODO: rename this function in zerokit rln-wasm
const memKeys = zerokitRLN.generateSeededExtendedMembershipKey(
this.zkRLN,
seedBytes
);
return IdentityCredential.fromBytes(memKeys);
}
public insertMember(idCommitment: Uint8Array): void {
zerokitRLN.insertMember(this.zkRLN, idCommitment);
}
public insertMembers(
index: number,
...idCommitments: Array<Uint8Array>
): void {
// serializes a seq of IDCommitments to a byte seq
// the order of serialization is |id_commitment_len<8>|id_commitment<var>|
const idCommitmentLen = BytesUtils.writeUIntLE(
new Uint8Array(8),
idCommitments.length,
0,
8
);
const idCommitmentBytes = BytesUtils.concatenate(
idCommitmentLen,
...idCommitments
);
zerokitRLN.setLeavesFrom(this.zkRLN, index, idCommitmentBytes);
}
public deleteMember(index: number): void {
zerokitRLN.deleteLeaf(this.zkRLN, index);
}
public getMerkleRoot(): Uint8Array {
return zerokitRLN.getRoot(this.zkRLN);
}
public serializeMessage(
uint8Msg: Uint8Array,
memIndex: number,
epoch: Uint8Array,
idKey: Uint8Array,
rateLimit?: number
): Uint8Array {
// calculate message length
const msgLen = BytesUtils.writeUIntLE(
new Uint8Array(8),
uint8Msg.length,
0,
8
);
const memIndexBytes = BytesUtils.writeUIntLE(
new Uint8Array(8),
memIndex,
0,
8
);
const rateLimitBytes = BytesUtils.writeUIntLE(
new Uint8Array(8),
rateLimit ?? this.rateLimit,
0,
8
);
// [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal<var> | rate_limit<8> ]
return BytesUtils.concatenate(
idKey,
memIndexBytes,
epoch,
msgLen,
uint8Msg,
rateLimitBytes
);
}
public async generateRLNProof(
msg: Uint8Array,
index: number,
epoch: Uint8Array | Date | undefined,
idSecretHash: Uint8Array,
rateLimit?: number
): Promise<IRateLimitProof> {
if (epoch === undefined) {
epoch = epochIntToBytes(dateToEpoch(new Date()));
} else if (epoch instanceof Date) {
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,
effectiveRateLimit
);
const rlnWitness = zerokitRLN.getSerializedRLNWitness(
this.zkRLN,
serialized_msg
);
const inputs = zerokitRLN.RLNWitnessToJson(this.zkRLN, rlnWitness);
const calculatedWitness = await this.witnessCalculator.calculateWitness(
inputs,
false
);
const proofBytes = zerokitRLN.generate_rln_proof_with_witness(
this.zkRLN,
calculatedWitness,
rlnWitness
);
return new Proof(proofBytes);
}
public verifyRLNProof(
proof: IRateLimitProof | Uint8Array,
msg: Uint8Array,
rateLimit?: number
): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
}
// calculate message length
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
const rateLimitBytes = BytesUtils.writeUIntLE(
new Uint8Array(8),
rateLimit ?? this.rateLimit,
0,
8
);
return zerokitRLN.verifyRLNProof(
this.zkRLN,
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes)
);
}
public verifyWithRoots(
proof: IRateLimitProof | Uint8Array,
msg: Uint8Array,
roots: Array<Uint8Array>,
rateLimit?: number
): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
}
// calculate message length
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
const rateLimitBytes = BytesUtils.writeUIntLE(
new Uint8Array(8),
rateLimit ?? this.rateLimit,
0,
8
);
const rootsBytes = BytesUtils.concatenate(...roots);
return zerokitRLN.verifyWithRoots(
this.zkRLN,
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
rootsBytes
);
}
public verifyWithNoRoot(
proof: IRateLimitProof | Uint8Array,
msg: Uint8Array,
rateLimit?: number
): boolean {
let pBytes: Uint8Array;
if (proof instanceof Uint8Array) {
pBytes = proof;
} else {
pBytes = proofToBytes(proof);
}
// calculate message length
const msgLen = BytesUtils.writeUIntLE(new Uint8Array(8), msg.length, 0, 8);
const rateLimitBytes = BytesUtils.writeUIntLE(
new Uint8Array(8),
rateLimit ?? this.rateLimit,
0,
8
);
return zerokitRLN.verifyWithRoots(
this.zkRLN,
BytesUtils.concatenate(pBytes, msgLen, msg, rateLimitBytes),
new Uint8Array()
);
}
}

View File

@ -95,6 +95,7 @@ describe("QueryOnConnect", () => {
it("should create QueryOnConnect instance with all required parameters", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -108,6 +109,7 @@ describe("QueryOnConnect", () => {
it("should create QueryOnConnect instance without options", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator
@ -120,6 +122,7 @@ describe("QueryOnConnect", () => {
it("should accept empty decoders array", () => {
queryOnConnect = new QueryOnConnect(
[],
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -134,6 +137,7 @@ describe("QueryOnConnect", () => {
beforeEach(() => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -173,6 +177,7 @@ describe("QueryOnConnect", () => {
beforeEach(() => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -224,6 +229,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -276,6 +282,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -298,6 +305,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -320,6 +328,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -391,6 +400,7 @@ describe("QueryOnConnect", () => {
const queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -418,6 +428,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -473,6 +484,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -605,6 +617,7 @@ describe("QueryOnConnect", () => {
queryOnConnect = new QueryOnConnect(
mockDecoders,
() => false,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
@ -750,6 +763,248 @@ describe("QueryOnConnect", () => {
expect(mockQueryGenerator.calledTwice).to.be.true;
});
});
describe("stopIfTrue predicate", () => {
beforeEach(() => {
mockPeerManagerEventEmitter.addEventListener = sinon.stub();
mockWakuEventEmitter.addEventListener = sinon.stub();
});
it("should stop query iteration when stopIfTrue returns true", async () => {
const messages = [
{
hash: new Uint8Array(),
hashStr: "msg1",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([1]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "stop-hash",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([2]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "msg3",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([3]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
// Setup generator to yield 3 pages, stop should occur on page 2
const mockAsyncGenerator = async function* (): AsyncGenerator<
Promise<IDecodedMessage | undefined>[]
> {
yield [Promise.resolve(messages[0])];
yield [Promise.resolve(messages[1])];
yield [Promise.resolve(messages[2])];
};
mockQueryGenerator.returns(mockAsyncGenerator());
const stopPredicate = (msg: IDecodedMessage): boolean =>
msg.hashStr === "stop-hash";
queryOnConnect = new QueryOnConnect(
mockDecoders,
stopPredicate,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
options
);
const receivedMessages: IDecodedMessage[] = [];
queryOnConnect.addEventListener(
QueryOnConnectEvent.MessagesRetrieved,
(event: CustomEvent<IDecodedMessage[]>) => {
receivedMessages.push(...event.detail);
}
);
queryOnConnect.start();
await queryOnConnect["maybeQuery"](mockPeerId);
// Should have received messages from first 2 pages only
expect(receivedMessages).to.have.length(2);
expect(receivedMessages[0].hashStr).to.equal("msg1");
expect(receivedMessages[1].hashStr).to.equal("stop-hash");
});
it("should process all pages when stopIfTrue never returns true", async () => {
const messages = [
{
hash: new Uint8Array(),
hashStr: "msg1",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([1]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "msg2",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([2]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "msg3",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([3]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
const mockAsyncGenerator = async function* (): AsyncGenerator<
Promise<IDecodedMessage | undefined>[]
> {
yield [Promise.resolve(messages[0])];
yield [Promise.resolve(messages[1])];
yield [Promise.resolve(messages[2])];
};
mockQueryGenerator.returns(mockAsyncGenerator());
const stopPredicate = (): boolean => false;
queryOnConnect = new QueryOnConnect(
mockDecoders,
stopPredicate,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
options
);
const receivedMessages: IDecodedMessage[] = [];
queryOnConnect.addEventListener(
QueryOnConnectEvent.MessagesRetrieved,
(event: CustomEvent<IDecodedMessage[]>) => {
receivedMessages.push(...event.detail);
}
);
queryOnConnect.start();
await queryOnConnect["maybeQuery"](mockPeerId);
// Should have received all 3 messages
expect(receivedMessages).to.have.length(3);
});
it("should stop on first message of a page if stopIfTrue matches", async () => {
const messages = [
{
hash: new Uint8Array(),
hashStr: "stop-hash",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([1]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "msg2",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([2]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: new Uint8Array(),
hashStr: "msg3",
version: 1,
timestamp: new Date(),
contentTopic: "/test/1/content",
pubsubTopic: "/waku/2/default-waku/proto",
payload: new Uint8Array([3]),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
const mockAsyncGenerator = async function* (): AsyncGenerator<
Promise<IDecodedMessage | undefined>[]
> {
yield [
Promise.resolve(messages[0]),
Promise.resolve(messages[1]),
Promise.resolve(messages[2])
];
};
mockQueryGenerator.returns(mockAsyncGenerator());
const stopPredicate = (msg: IDecodedMessage): boolean =>
msg.hashStr === "stop-hash";
queryOnConnect = new QueryOnConnect(
mockDecoders,
stopPredicate,
mockPeerManagerEventEmitter,
mockWakuEventEmitter,
mockQueryGenerator,
options
);
const receivedMessages: IDecodedMessage[] = [];
queryOnConnect.addEventListener(
QueryOnConnectEvent.MessagesRetrieved,
(event: CustomEvent<IDecodedMessage[]>) => {
receivedMessages.push(...event.detail);
}
);
queryOnConnect.start();
await queryOnConnect["maybeQuery"](mockPeerId);
// Should have received all 3 messages from the page, even though first matched
expect(receivedMessages).to.have.length(3);
expect(receivedMessages[0].hashStr).to.equal("stop-hash");
expect(receivedMessages[1].hashStr).to.equal("msg2");
expect(receivedMessages[2].hashStr).to.equal("msg3");
});
});
});
describe("calculateTimeRange", () => {

View File

@ -17,7 +17,7 @@ import {
const log = new Logger("sdk:query-on-connect");
export const DEFAULT_FORCE_QUERY_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
export const MAX_TIME_RANGE_QUERY_MS = 24 * 60 * 60 * 1000; // 24 hours
export const MAX_TIME_RANGE_QUERY_MS = 30 * 24 * 60 * 60 * 1000; // 30 days (queries are split)
export interface QueryOnConnectOptions {
/**
@ -54,6 +54,7 @@ export class QueryOnConnect<
public constructor(
public decoders: IDecoder<T>[],
public stopIfTrue: (msg: T) => boolean,
private readonly peerManagerEventEmitter: TypedEventEmitter<IPeerManagerEvents>,
private readonly wakuEventEmitter: IWakuEventEmitter,
private readonly _queryGenerator: <T extends IDecodedMessage>(
@ -125,8 +126,13 @@ export class QueryOnConnect<
const messages = (await Promise.all(page)).filter(
(m) => m !== undefined
);
const stop = messages.some((msg: T) => this.stopIfTrue(msg));
// Bundle the messages to help batch process by sds
this.dispatchMessages(messages);
if (stop) {
break;
}
}
// Didn't throw, so it didn't fail

View File

@ -13,7 +13,7 @@ import {
LightPushSDKResult,
QueryRequestParams
} from "@waku/interfaces";
import { ContentMessage } from "@waku/sds";
import { ContentMessage, SyncMessage } from "@waku/sds";
import {
createRoutingInfo,
delay,
@ -176,7 +176,8 @@ describe("Reliable Channel", () => {
expect(messageAcknowledged).to.be.false;
});
it("Outgoing message is possibly acknowledged", async () => {
// TODO: https://github.com/waku-org/js-waku/issues/2648
it.skip("Outgoing message is possibly acknowledged", async () => {
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);
@ -418,7 +419,7 @@ describe("Reliable Channel", () => {
"MyChannel",
"alice",
[],
1,
1n,
undefined,
message
);
@ -531,7 +532,7 @@ describe("Reliable Channel", () => {
"testChannel",
"testSender",
[],
1,
1n,
undefined,
messagePayload
);
@ -599,7 +600,7 @@ describe("Reliable Channel", () => {
"testChannel",
"testSender",
[],
1,
1n,
undefined,
message1Payload
);
@ -609,7 +610,7 @@ describe("Reliable Channel", () => {
"testChannel",
"testSender",
[],
2,
2n,
undefined,
message2Payload
);
@ -677,4 +678,456 @@ describe("Reliable Channel", () => {
expect(queryGeneratorStub.called).to.be.true;
});
});
describe("stopIfTrue Integration with QueryOnConnect", () => {
let mockWakuNode: MockWakuNode;
let encoder: IEncoder;
let decoder: IDecoder<IDecodedMessage>;
let mockPeerManagerEvents: TypedEventEmitter<any>;
let queryGeneratorStub: sinon.SinonStub;
let mockPeerId: PeerId;
beforeEach(async () => {
mockWakuNode = new MockWakuNode();
mockPeerManagerEvents = new TypedEventEmitter();
(mockWakuNode as any).peerManager = {
events: mockPeerManagerEvents
};
encoder = createEncoder({
contentTopic: TEST_CONTENT_TOPIC,
routingInfo: TEST_ROUTING_INFO
});
decoder = createDecoder(TEST_CONTENT_TOPIC, TEST_ROUTING_INFO);
queryGeneratorStub = sinon.stub();
mockWakuNode.store = {
queryGenerator: queryGeneratorStub
} as any;
mockPeerId = {
toString: () => "QmTestPeerId"
} as unknown as PeerId;
});
it("should stop query when sync message from same channel is found", async () => {
const channelId = "testChannel";
const senderId = "testSender";
// Create messages: one from different channel, one sync from same channel, one more
const sdsMessageDifferentChannel = new ContentMessage(
"msg1",
"differentChannel",
senderId,
[],
1n,
undefined,
utf8ToBytes("different channel")
);
const sdsSyncMessage = new SyncMessage(
"sync-msg-id",
channelId,
senderId,
[],
2n,
undefined,
undefined
);
const sdsMessageAfterSync = new ContentMessage(
"msg3",
channelId,
senderId,
[],
3n,
undefined,
utf8ToBytes("after sync")
);
const messages: IDecodedMessage[] = [
{
hash: hexToBytes("1111"),
hashStr: "1111",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageDifferentChannel.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: hexToBytes("2222"),
hashStr: "2222",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsSyncMessage.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: hexToBytes("3333"),
hashStr: "3333",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageAfterSync.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
// Setup generator to yield 3 messages, but should stop after 2nd
queryGeneratorStub.callsFake(async function* () {
yield [Promise.resolve(messages[0])];
yield [Promise.resolve(messages[1])];
yield [Promise.resolve(messages[2])];
});
const reliableChannel = await ReliableChannel.create(
mockWakuNode,
channelId,
senderId,
encoder,
decoder
);
await delay(50);
// Trigger query on connect
mockPeerManagerEvents.dispatchEvent(
new CustomEvent("store:connect", { detail: mockPeerId })
);
await delay(200);
// queryGenerator should have been called
expect(queryGeneratorStub.called).to.be.true;
// The query should have stopped after finding sync message from same channel
expect(reliableChannel).to.not.be.undefined;
});
it("should stop query on content message from same channel", async () => {
const channelId = "testChannel";
const senderId = "testSender";
const sdsContentMessage = new ContentMessage(
"msg1",
channelId,
senderId,
[{ messageId: "previous-msg-id" }],
1n,
undefined,
utf8ToBytes("content message")
);
const sdsMessageAfter = new ContentMessage(
"msg2",
channelId,
senderId,
[],
2n,
undefined,
utf8ToBytes("after content")
);
const messages: IDecodedMessage[] = [
{
hash: hexToBytes("1111"),
hashStr: "1111",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsContentMessage.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: hexToBytes("2222"),
hashStr: "2222",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageAfter.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
let pagesYielded = 0;
queryGeneratorStub.callsFake(async function* () {
pagesYielded++;
yield [Promise.resolve(messages[0])];
pagesYielded++;
yield [Promise.resolve(messages[1])];
});
const reliableChannel = await ReliableChannel.create(
mockWakuNode,
channelId,
senderId,
encoder,
decoder
);
await delay(50);
mockPeerManagerEvents.dispatchEvent(
new CustomEvent("store:connect", { detail: mockPeerId })
);
await delay(200);
expect(queryGeneratorStub.called).to.be.true;
expect(reliableChannel).to.not.be.undefined;
// Should have stopped after first page with content message
expect(pagesYielded).to.equal(1);
});
it("should continue query when messages are from different channels", async () => {
const channelId = "testChannel";
const senderId = "testSender";
const sdsMessageDifferent1 = new ContentMessage(
"msg1",
"differentChannel1",
senderId,
[],
1n,
undefined,
utf8ToBytes("different 1")
);
const sdsMessageDifferent2 = new ContentMessage(
"msg2",
"differentChannel2",
senderId,
[],
2n,
undefined,
utf8ToBytes("different 2")
);
const sdsMessageDifferent3 = new ContentMessage(
"msg3",
"differentChannel3",
senderId,
[],
3n,
undefined,
utf8ToBytes("different 3")
);
const messages: IDecodedMessage[] = [
{
hash: hexToBytes("1111"),
hashStr: "1111",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageDifferent1.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: hexToBytes("2222"),
hashStr: "2222",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageDifferent2.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
},
{
hash: hexToBytes("3333"),
hashStr: "3333",
version: 1,
timestamp: new Date(),
contentTopic: TEST_CONTENT_TOPIC,
pubsubTopic: decoder.pubsubTopic,
payload: sdsMessageDifferent3.encode(),
rateLimitProof: undefined,
ephemeral: false,
meta: undefined
}
];
let pagesYielded = 0;
queryGeneratorStub.callsFake(async function* () {
pagesYielded++;
yield [Promise.resolve(messages[0])];
pagesYielded++;
yield [Promise.resolve(messages[1])];
pagesYielded++;
yield [Promise.resolve(messages[2])];
});
const reliableChannel = await ReliableChannel.create(
mockWakuNode,
channelId,
senderId,
encoder,
decoder
);
await delay(50);
mockPeerManagerEvents.dispatchEvent(
new CustomEvent("store:connect", { detail: mockPeerId })
);
await delay(200);
expect(queryGeneratorStub.called).to.be.true;
expect(reliableChannel).to.not.be.undefined;
// Should have processed all pages since no matching channel
expect(pagesYielded).to.equal(3);
});
});
describe("isChannelMessageWithCausalHistory predicate", () => {
let mockWakuNode: MockWakuNode;
let reliableChannel: ReliableChannel<IDecodedMessage>;
let encoder: IEncoder;
let decoder: IDecoder<IDecodedMessage>;
beforeEach(async () => {
mockWakuNode = new MockWakuNode();
encoder = createEncoder({
contentTopic: TEST_CONTENT_TOPIC,
routingInfo: TEST_ROUTING_INFO
});
decoder = createDecoder(TEST_CONTENT_TOPIC, TEST_ROUTING_INFO);
reliableChannel = await ReliableChannel.create(
mockWakuNode,
"testChannel",
"testSender",
encoder,
decoder,
{ queryOnConnect: false }
);
});
it("should return false for malformed SDS messages", () => {
const msg = {
payload: new Uint8Array([1, 2, 3])
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.false;
});
it("should return false for different channelId", () => {
const sdsMsg = new ContentMessage(
"msg1",
"differentChannel",
"sender",
[],
1n,
undefined,
utf8ToBytes("content")
);
const msg = {
payload: sdsMsg.encode()
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.false;
});
it("should return false for sync message without causal history", () => {
const syncMsg = new SyncMessage(
"sync-msg-id",
"testChannel",
"sender",
[],
1n,
undefined,
undefined
);
const msg = {
payload: syncMsg.encode()
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.false;
});
it("should return false for content message without causal history", () => {
const contentMsg = new ContentMessage(
"msg1",
"testChannel",
"sender",
[],
1n,
undefined,
utf8ToBytes("content")
);
const msg = {
payload: contentMsg.encode()
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.false;
});
it("should return true for message with causal history", () => {
const contentMsg = new ContentMessage(
"msg1",
"testChannel",
"sender",
[{ messageId: "previous-msg-id" }],
1n,
undefined,
utf8ToBytes("content")
);
const msg = {
payload: contentMsg.encode()
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.true;
});
it("should return true for sync message with causal history", () => {
const syncMsg = new SyncMessage(
"sync-msg-id",
"testChannel",
"sender",
[{ messageId: "previous-msg-id" }],
1n,
undefined,
undefined
);
const msg = {
payload: syncMsg.encode()
} as IDecodedMessage;
const result = reliableChannel["isChannelMessageWithCausalHistory"](msg);
expect(result).to.be.true;
});
});
});

View File

@ -185,9 +185,9 @@ export class ReliableChannel<
peerManagerEvents !== undefined &&
(options?.queryOnConnect ?? true)
) {
log.info("auto-query enabled");
this.queryOnConnect = new QueryOnConnect(
[this.decoder],
this.isChannelMessageWithCausalHistory.bind(this),
peerManagerEvents,
node.events,
this._retrieve.bind(this)
@ -580,6 +580,21 @@ export class ReliableChannel<
this.messageChannel.sweepOutgoingBuffer();
}
private isChannelMessageWithCausalHistory(msg: T): boolean {
// TODO: we do end-up decoding messages twice as this is used to stop store queries.
const sdsMessage = SdsMessage.decode(msg.payload);
if (!sdsMessage) {
return false;
}
if (sdsMessage.channelId !== this.messageChannel.channelId) {
return false;
}
return sdsMessage.causalHistory && sdsMessage.causalHistory.length > 0;
}
private setupEventListeners(): void {
this.messageChannel.addEventListener(
MessageChannelEvent.OutMessageSent,

View File

@ -187,7 +187,8 @@ describe("Reliable Channel: Encryption", () => {
expect(messageAcknowledged).to.be.false;
});
it("Outgoing message is possibly acknowledged", async () => {
// TODO: https://github.com/waku-org/js-waku/issues/2648
it.skip("Outgoing message is possibly acknowledged", async () => {
const commonEventEmitter = new TypedEventEmitter<MockWakuEvents>();
const mockWakuNodeAlice = new MockWakuNode(commonEventEmitter);
const mockWakuNodeBob = new MockWakuNode(commonEventEmitter);

View File

@ -56,6 +56,19 @@ describe("Reliable Channel: Sync", () => {
}
);
// Send a message to have a history
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
let messageSent = false;
reliableChannel.addEventListener("message-sent", (event) => {
if (event.detail === sentMsgId) {
messageSent = true;
}
});
while (!messageSent) {
await delay(50);
}
let syncMessageSent = false;
reliableChannel.messageChannel.addEventListener(
MessageChannelEvent.OutSyncSent,
@ -131,6 +144,19 @@ describe("Reliable Channel: Sync", () => {
return 1;
}; // will wait a full second
// Send a message to have a history
const sentMsgId = reliableChannelAlice.send(utf8ToBytes("some message"));
let messageSent = false;
reliableChannelAlice.addEventListener("message-sent", (event) => {
if (event.detail === sentMsgId) {
messageSent = true;
}
});
while (!messageSent) {
await delay(50);
}
let syncMessageSent = false;
reliableChannelBob.messageChannel.addEventListener(
MessageChannelEvent.OutSyncSent,
@ -191,6 +217,19 @@ describe("Reliable Channel: Sync", () => {
return 1;
}; // will wait a full second
// Send a message to have a history
const sentMsgId = reliableChannelAlice.send(utf8ToBytes("some message"));
let messageSent = false;
reliableChannelAlice.addEventListener("message-sent", (event) => {
if (event.detail === sentMsgId) {
messageSent = true;
}
});
while (!messageSent) {
await delay(50);
}
let syncMessageSent = false;
reliableChannelBob.messageChannel.addEventListener(
MessageChannelEvent.OutSyncSent,
@ -232,6 +271,19 @@ describe("Reliable Channel: Sync", () => {
return 1;
}; // will wait a full second
// Send a message to have a history
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
let messageSent = false;
reliableChannel.addEventListener("message-sent", (event) => {
if (event.detail === sentMsgId) {
messageSent = true;
}
});
while (!messageSent) {
await delay(50);
}
let syncMessageSent = false;
reliableChannel.messageChannel.addEventListener(
MessageChannelEvent.OutSyncSent,
@ -273,6 +325,19 @@ describe("Reliable Channel: Sync", () => {
return 1;
}; // will wait a full second
// Send a message to have a history
const sentMsgId = reliableChannel.send(utf8ToBytes("some message"));
let messageSent = false;
reliableChannel.addEventListener("message-sent", (event) => {
if (event.detail === sentMsgId) {
messageSent = true;
}
});
while (!messageSent) {
await delay(50);
}
let syncMessageSent = false;
reliableChannel.messageChannel.addEventListener(
MessageChannelEvent.OutSyncSent,

View File

@ -0,0 +1,56 @@
import { expect } from "chai";
import { lamportTimestampIncrement } from "./message_channel.js";
describe("lamportTimestampIncrement", () => {
it("should increment timestamp by 1 when current time is not greater", () => {
const futureTimestamp = BigInt(Date.now()) + 1000n;
const result = lamportTimestampIncrement(futureTimestamp);
expect(result).to.equal(futureTimestamp + 1n);
});
it("should use current time when it's greater than incremented timestamp", () => {
const pastTimestamp = BigInt(Date.now()) - 1000n;
const result = lamportTimestampIncrement(pastTimestamp);
const now = BigInt(Date.now());
// Result should be at least as large as now (within small tolerance for test execution time)
expect(result >= now - 10n).to.be.true;
expect(result <= now + 10n).to.be.true;
});
it("should handle timestamp equal to current time", () => {
const currentTimestamp = BigInt(Date.now());
const result = lamportTimestampIncrement(currentTimestamp);
// Should increment by 1 since now is likely not greater than current + 1
expect(result >= currentTimestamp + 1n).to.be.true;
});
it("should ensure monotonic increase", () => {
let timestamp = BigInt(Date.now()) + 5000n;
const results: bigint[] = [];
for (let i = 0; i < 5; i++) {
timestamp = lamportTimestampIncrement(timestamp);
results.push(timestamp);
}
// Verify all timestamps are strictly increasing
for (let i = 1; i < results.length; i++) {
expect(results[i] > results[i - 1]).to.be.true;
}
});
it("should handle very large timestamps", () => {
const largeTimestamp = BigInt(Number.MAX_SAFE_INTEGER) * 1000n;
const result = lamportTimestampIncrement(largeTimestamp);
expect(result).to.equal(largeTimestamp + 1n);
});
it("should jump to current time when timestamp is far in the past", () => {
const veryOldTimestamp = 1000n; // Very old timestamp (1 second after epoch)
const result = lamportTimestampIncrement(veryOldTimestamp);
const now = BigInt(Date.now());
expect(result >= now - 10n).to.be.true;
expect(result <= now + 10n).to.be.true;
});
});

View File

@ -18,7 +18,7 @@ describe("Message serialization", () => {
"my-channel",
"me",
[],
0,
0n,
bloomFilter.toBytes(),
undefined
);
@ -42,7 +42,7 @@ describe("Message serialization", () => {
"my-channel",
"me",
[{ messageId: depMessageId, retrievalHint: depRetrievalHint }],
0,
0n,
undefined,
undefined
);
@ -63,7 +63,7 @@ describe("ContentMessage comparison with < operator", () => {
"channel",
"sender",
[],
100, // Lower timestamp
100n, // Lower timestamp
undefined,
new Uint8Array([1])
);
@ -73,7 +73,7 @@ describe("ContentMessage comparison with < operator", () => {
"channel",
"sender",
[],
200, // Higher timestamp
200n, // Higher timestamp
undefined,
new Uint8Array([2])
);
@ -89,7 +89,7 @@ describe("ContentMessage comparison with < operator", () => {
"channel",
"sender",
[],
100, // Same timestamp
100n, // Same timestamp
undefined,
new Uint8Array([1])
);
@ -99,7 +99,7 @@ describe("ContentMessage comparison with < operator", () => {
"channel",
"sender",
[],
100, // Same timestamp
100n, // Same timestamp
undefined,
new Uint8Array([2])
);

View File

@ -14,7 +14,7 @@ export class Message implements proto_sds_message.SdsMessage {
public channelId: string,
public senderId: string,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp?: number | undefined,
public lamportTimestamp?: bigint | undefined,
public bloomFilter?: Uint8Array<ArrayBufferLike> | undefined,
public content?: Uint8Array<ArrayBufferLike> | undefined,
/**
@ -30,56 +30,60 @@ export class Message implements proto_sds_message.SdsMessage {
public static decode(
data: Uint8Array
): undefined | ContentMessage | SyncMessage | EphemeralMessage {
const {
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp,
bloomFilter,
content
} = proto_sds_message.SdsMessage.decode(data);
if (testContentMessage({ lamportTimestamp, content })) {
return new ContentMessage(
try {
const {
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
lamportTimestamp,
bloomFilter,
content!
);
}
content
} = proto_sds_message.SdsMessage.decode(data);
if (testEphemeralMessage({ lamportTimestamp, content })) {
return new EphemeralMessage(
messageId,
channelId,
senderId,
causalHistory,
undefined,
bloomFilter,
content!
);
}
if (testContentMessage({ lamportTimestamp, content })) {
return new ContentMessage(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
bloomFilter,
content!
);
}
if (testSyncMessage({ lamportTimestamp, content })) {
return new SyncMessage(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
bloomFilter,
undefined
if (testEphemeralMessage({ lamportTimestamp, content })) {
return new EphemeralMessage(
messageId,
channelId,
senderId,
causalHistory,
undefined,
bloomFilter,
content!
);
}
if (testSyncMessage({ lamportTimestamp, content })) {
return new SyncMessage(
messageId,
channelId,
senderId,
causalHistory,
lamportTimestamp!,
bloomFilter,
undefined
);
}
log.error(
"message received was of unknown type",
lamportTimestamp,
content
);
} catch (err) {
log.error("failed to decode sds message", err);
}
log.error(
"message received was of unknown type",
lamportTimestamp,
content
);
return undefined;
}
}
@ -90,7 +94,7 @@ export class SyncMessage extends Message {
public channelId: string,
public senderId: string,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp: number,
public lamportTimestamp: bigint,
public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
public content: undefined,
/**
@ -112,12 +116,12 @@ export class SyncMessage extends Message {
}
function testSyncMessage(message: {
lamportTimestamp?: number;
lamportTimestamp?: bigint;
content?: Uint8Array;
}): boolean {
return Boolean(
"lamportTimestamp" in message &&
typeof message.lamportTimestamp === "number" &&
typeof message.lamportTimestamp === "bigint" &&
(message.content === undefined || message.content.length === 0)
);
}
@ -165,7 +169,7 @@ export function isEphemeralMessage(
}
function testEphemeralMessage(message: {
lamportTimestamp?: number;
lamportTimestamp?: bigint;
content?: Uint8Array;
}): boolean {
return Boolean(
@ -182,7 +186,7 @@ export class ContentMessage extends Message {
public channelId: string,
public senderId: string,
public causalHistory: proto_sds_message.HistoryEntry[],
public lamportTimestamp: number,
public lamportTimestamp: bigint,
public bloomFilter: Uint8Array<ArrayBufferLike> | undefined,
public content: Uint8Array<ArrayBufferLike>,
/**
@ -222,12 +226,12 @@ export function isContentMessage(
}
function testContentMessage(message: {
lamportTimestamp?: number;
lamportTimestamp?: bigint;
content?: Uint8Array;
}): message is { lamportTimestamp: number; content: Uint8Array } {
}): message is { lamportTimestamp: bigint; content: Uint8Array } {
return Boolean(
"lamportTimestamp" in message &&
typeof message.lamportTimestamp === "number" &&
typeof message.lamportTimestamp === "bigint" &&
message.content &&
message.content.length
);

View File

@ -75,7 +75,7 @@ describe("MessageChannel", function () {
const timestampBefore = channelA["lamportTimestamp"];
await sendMessage(channelA, utf8ToBytes("message"), callback);
const timestampAfter = channelA["lamportTimestamp"];
expect(timestampAfter).to.equal(timestampBefore + 1);
expect(timestampAfter).to.equal(timestampBefore + 1n);
});
it("should push the message to the outgoing buffer", async () => {
@ -95,7 +95,7 @@ describe("MessageChannel", function () {
it("should insert message id into causal history", async () => {
const payload = utf8ToBytes("message");
const expectedTimestamp = channelA["lamportTimestamp"] + 1;
const expectedTimestamp = channelA["lamportTimestamp"] + 1n;
const messageId = MessageChannel.getMessageId(payload);
await sendMessage(channelA, payload, callback);
const messageIdLog = channelA["localHistory"] as ILocalHistory;
@ -181,7 +181,7 @@ describe("MessageChannel", function () {
return { success: true };
});
const timestampAfter = channelA["lamportTimestamp"];
expect(timestampAfter).to.equal(timestampBefore + 1);
expect(timestampAfter).to.equal(timestampBefore + 1n);
});
// TODO: test is failing in CI, investigate in https://github.com/waku-org/js-waku/issues/2648
@ -201,7 +201,9 @@ describe("MessageChannel", function () {
});
}
const timestampAfter = testChannelA["lamportTimestamp"];
expect(timestampAfter - timestampBefore).to.equal(messagesB.length);
expect(timestampAfter - timestampBefore).to.equal(
BigInt(messagesB.length)
);
});
// TODO: test is failing in CI, investigate in https://github.com/waku-org/js-waku/issues/2648
@ -228,7 +230,7 @@ describe("MessageChannel", function () {
const expectedLength = messagesA.length + messagesB.length;
expect(channelA["lamportTimestamp"]).to.equal(
aTimestampBefore + expectedLength
aTimestampBefore + BigInt(expectedLength)
);
expect(channelA["lamportTimestamp"]).to.equal(
channelB["lamportTimestamp"]
@ -293,7 +295,7 @@ describe("MessageChannel", function () {
channelA.channelId,
"not-alice",
[],
1,
1n,
undefined,
payload,
testRetrievalHint
@ -335,7 +337,7 @@ describe("MessageChannel", function () {
channelA.channelId,
"bob",
[],
startTimestamp + 3, // Higher timestamp
startTimestamp + 3n, // Higher timestamp
undefined,
message3Payload
)
@ -349,7 +351,7 @@ describe("MessageChannel", function () {
channelA.channelId,
"carol",
[],
startTimestamp + 2, // Middle timestamp
startTimestamp + 2n, // Middle timestamp
undefined,
message2Payload
)
@ -363,7 +365,7 @@ describe("MessageChannel", function () {
const first = localHistory.findIndex(
({ messageId, lamportTimestamp }) => {
return (
messageId === message1Id && lamportTimestamp === startTimestamp + 1
messageId === message1Id && lamportTimestamp === startTimestamp + 1n
);
}
);
@ -372,7 +374,7 @@ describe("MessageChannel", function () {
const second = localHistory.findIndex(
({ messageId, lamportTimestamp }) => {
return (
messageId === message2Id && lamportTimestamp === startTimestamp + 2
messageId === message2Id && lamportTimestamp === startTimestamp + 2n
);
}
);
@ -381,7 +383,7 @@ describe("MessageChannel", function () {
const third = localHistory.findIndex(
({ messageId, lamportTimestamp }) => {
return (
messageId === message3Id && lamportTimestamp === startTimestamp + 3
messageId === message3Id && lamportTimestamp === startTimestamp + 3n
);
}
);
@ -404,7 +406,7 @@ describe("MessageChannel", function () {
channelA.channelId,
"bob",
[],
5, // Same timestamp
5n, // Same timestamp
undefined,
message2Payload
)
@ -417,7 +419,7 @@ describe("MessageChannel", function () {
channelA.channelId,
"carol",
[],
5, // Same timestamp
5n, // Same timestamp
undefined,
message1Payload
)
@ -432,14 +434,14 @@ describe("MessageChannel", function () {
const first = localHistory.findIndex(
({ messageId, lamportTimestamp }) => {
return messageId === expectedOrder[0] && lamportTimestamp == 5;
return messageId === expectedOrder[0] && lamportTimestamp == 5n;
}
);
expect(first).to.eq(0);
const second = localHistory.findIndex(
({ messageId, lamportTimestamp }) => {
return messageId === expectedOrder[1] && lamportTimestamp == 5;
return messageId === expectedOrder[1] && lamportTimestamp == 5n;
}
);
expect(second).to.eq(1);
@ -645,11 +647,12 @@ describe("MessageChannel", function () {
});
// And be sends a sync message
await channelB.pushOutgoingSyncMessage(async (message) => {
const res = await channelB.pushOutgoingSyncMessage(async (message) => {
await receiveMessage(channelA, message);
return true;
});
expect(res).to.be.true;
expect(messageAcked).to.be.true;
});
});
@ -1087,17 +1090,41 @@ describe("MessageChannel", function () {
causalHistorySize: 2
});
channelB = new MessageChannel(channelId, "bob", { causalHistorySize: 2 });
const message = utf8ToBytes("first message in channel");
channelA["localHistory"].push(
new ContentMessage(
MessageChannel.getMessageId(message),
"MyChannel",
"alice",
[],
1n,
undefined,
message
)
);
});
it("should be sent with empty content", async () => {
await channelA.pushOutgoingSyncMessage(async (message) => {
const res = await channelA.pushOutgoingSyncMessage(async (message) => {
expect(message.content).to.be.undefined;
return true;
});
expect(res).to.be.true;
});
it("should not be sent when there is no history", async () => {
const channelC = new MessageChannel(channelId, "carol", {
causalHistorySize: 2
});
const res = await channelC.pushOutgoingSyncMessage(async (_msg) => {
throw "callback was called when it's not expected";
});
expect(res).to.be.false;
});
it("should not be added to outgoing buffer, bloom filter, or local log", async () => {
await channelA.pushOutgoingSyncMessage();
const res = await channelA.pushOutgoingSyncMessage();
expect(res).to.be.true;
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
expect(outgoingBuffer.length).to.equal(0);
@ -1108,15 +1135,16 @@ describe("MessageChannel", function () {
).to.equal(false);
const localLog = channelA["localHistory"];
expect(localLog.length).to.equal(0);
expect(localLog.length).to.equal(1); // beforeEach adds one message
});
it("should not be delivered", async () => {
const timestampBefore = channelB["lamportTimestamp"];
await channelA.pushOutgoingSyncMessage(async (message) => {
const res = await channelA.pushOutgoingSyncMessage(async (message) => {
await receiveMessage(channelB, message);
return true;
});
expect(res).to.be.true;
const timestampAfter = channelB["lamportTimestamp"];
expect(timestampAfter).to.equal(timestampBefore);
@ -1130,20 +1158,23 @@ describe("MessageChannel", function () {
});
it("should update ack status of messages in outgoing buffer", async () => {
const channelC = new MessageChannel(channelId, "carol", {
causalHistorySize: 2
});
for (const m of messagesA) {
await sendMessage(channelA, utf8ToBytes(m), async (message) => {
await sendMessage(channelC, utf8ToBytes(m), async (message) => {
await receiveMessage(channelB, message);
return { success: true };
});
}
await sendSyncMessage(channelB, async (message) => {
await receiveMessage(channelA, message);
await receiveMessage(channelC, message);
return true;
});
const causalHistorySize = channelA["causalHistorySize"];
const outgoingBuffer = channelA["outgoingBuffer"] as Message[];
const causalHistorySize = channelC["causalHistorySize"];
const outgoingBuffer = channelC["outgoingBuffer"] as Message[];
expect(outgoingBuffer.length).to.equal(
messagesA.length - causalHistorySize
);

View File

@ -56,7 +56,7 @@ export type ILocalHistory = Pick<
export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
public readonly channelId: ChannelId;
public readonly senderId: SenderId;
private lamportTimestamp: number;
private lamportTimestamp: bigint;
private filter: DefaultBloomFilter;
private outgoingBuffer: ContentMessage[];
private possibleAcks: Map<MessageId, number>;
@ -95,9 +95,8 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
super();
this.channelId = channelId;
this.senderId = senderId;
// SDS RFC says to use nanoseconds, but current time in nanosecond is > Number.MAX_SAFE_INTEGER
// So instead we are using milliseconds and proposing a spec change (TODO)
this.lamportTimestamp = Date.now();
// Initialize channel lamport timestamp to current time in milliseconds.
this.lamportTimestamp = BigInt(Date.now());
this.filter = new DefaultBloomFilter(DEFAULT_BLOOM_FILTER_OPTIONS);
this.outgoingBuffer = [];
this.possibleAcks = new Map();
@ -369,7 +368,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
public async pushOutgoingSyncMessage(
callback?: (message: SyncMessage) => Promise<boolean>
): Promise<boolean> {
this.lamportTimestamp++;
this.lamportTimestamp = lamportTimestampIncrement(this.lamportTimestamp);
const message = new SyncMessage(
// does not need to be secure randomness
`sync-${Math.random().toString(36).substring(2)}`,
@ -385,6 +384,14 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
undefined
);
if (!message.causalHistory || message.causalHistory.length === 0) {
log.info(
this.senderId,
"no causal history in sync message, aborting sending"
);
return false;
}
if (callback) {
try {
await callback(message);
@ -401,7 +408,8 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
throw error;
}
}
return false;
// No problem encountered so returning true
return true;
}
private _pushIncomingMessage(message: Message): void {
@ -526,7 +534,7 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
retrievalHint?: Uint8Array;
}>
): Promise<void> {
this.lamportTimestamp++;
this.lamportTimestamp = lamportTimestampIncrement(this.lamportTimestamp);
const messageId = MessageChannel.getMessageId(payload);
@ -724,3 +732,12 @@ export class MessageChannel extends TypedEventEmitter<MessageChannelEvents> {
});
}
}
export function lamportTimestampIncrement(lamportTimestamp: bigint): bigint {
const now = BigInt(Date.now());
lamportTimestamp++;
if (now > lamportTimestamp) {
return now;
}
return lamportTimestamp;
}