mirror of
https://github.com/status-im/js-waku.git
synced 2025-02-24 02:48:11 +00:00
feat: lightpush & filter send requests to multiple peers (#1779)
* feat: lightpush & filter send requests to multiple peers * build message-hash before core * chore: restructure folder heirrarchy * fix: imports * chore: move @waku/core to dev deps * feat: create a new `ServiceNodes` wrapper class to encapsulate service node and message collector with redundancy accounted for * chore(filter): move tests against single service node to a subdir * feat: support relay, add strict checking, add tests * fix(filter): handle errors * chore(tests): add tests for ping * add tests for push * chore: abstract redundancy * feat: add unsubscribe tests * fix: tests * add lightpush tests * fix: imports * fix: merge & add warning * merge: master * fix: breaking tests with master * address comments * make num peers configurable * fix: typo
This commit is contained in:
parent
3e7b95e604
commit
7affbe222d
4
package-lock.json
generated
4
package-lock.json
generated
@ -8,11 +8,11 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/interfaces",
|
"packages/interfaces",
|
||||||
"packages/utils",
|
"packages/utils",
|
||||||
|
"packages/message-hash",
|
||||||
"packages/proto",
|
"packages/proto",
|
||||||
"packages/enr",
|
"packages/enr",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/relay",
|
"packages/relay",
|
||||||
"packages/message-hash",
|
|
||||||
"packages/peer-exchange",
|
"packages/peer-exchange",
|
||||||
"packages/dns-discovery",
|
"packages/dns-discovery",
|
||||||
"packages/message-encryption",
|
"packages/message-encryption",
|
||||||
@ -27635,6 +27635,7 @@
|
|||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/enr": "^0.0.20",
|
"@waku/enr": "^0.0.20",
|
||||||
"@waku/interfaces": "0.0.21",
|
"@waku/interfaces": "0.0.21",
|
||||||
|
"@waku/message-hash": "^0.1.10",
|
||||||
"@waku/proto": "0.0.6",
|
"@waku/proto": "0.0.6",
|
||||||
"@waku/utils": "0.0.14",
|
"@waku/utils": "0.0.14",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@ -31818,6 +31819,7 @@
|
|||||||
"@waku/build-utils": "*",
|
"@waku/build-utils": "*",
|
||||||
"@waku/enr": "^0.0.20",
|
"@waku/enr": "^0.0.20",
|
||||||
"@waku/interfaces": "0.0.21",
|
"@waku/interfaces": "0.0.21",
|
||||||
|
"@waku/message-hash": "^0.1.10",
|
||||||
"@waku/proto": "0.0.6",
|
"@waku/proto": "0.0.6",
|
||||||
"@waku/utils": "0.0.14",
|
"@waku/utils": "0.0.14",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
|
@ -5,11 +5,11 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/interfaces",
|
"packages/interfaces",
|
||||||
"packages/utils",
|
"packages/utils",
|
||||||
|
"packages/message-hash",
|
||||||
"packages/proto",
|
"packages/proto",
|
||||||
"packages/enr",
|
"packages/enr",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/relay",
|
"packages/relay",
|
||||||
"packages/message-hash",
|
|
||||||
"packages/peer-exchange",
|
"packages/peer-exchange",
|
||||||
"packages/dns-discovery",
|
"packages/dns-discovery",
|
||||||
"packages/message-encryption",
|
"packages/message-encryption",
|
||||||
|
@ -76,6 +76,7 @@
|
|||||||
"@noble/hashes": "^1.3.2",
|
"@noble/hashes": "^1.3.2",
|
||||||
"@waku/enr": "^0.0.20",
|
"@waku/enr": "^0.0.20",
|
||||||
"@waku/interfaces": "0.0.21",
|
"@waku/interfaces": "0.0.21",
|
||||||
|
"@waku/message-hash": "^0.1.10",
|
||||||
"@waku/proto": "0.0.6",
|
"@waku/proto": "0.0.6",
|
||||||
"@waku/utils": "0.0.14",
|
"@waku/utils": "0.0.14",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
|
@ -17,6 +17,8 @@ import {
|
|||||||
import { filterPeersByDiscovery } from "./filterPeers.js";
|
import { filterPeersByDiscovery } from "./filterPeers.js";
|
||||||
import { StreamManager } from "./stream_manager.js";
|
import { StreamManager } from "./stream_manager.js";
|
||||||
|
|
||||||
|
const DEFAULT_NUM_PEERS_TO_USE = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class with predefined helpers, to be used as a base to implement Waku
|
* A class with predefined helpers, to be used as a base to implement Waku
|
||||||
* Protocols.
|
* Protocols.
|
||||||
@ -24,6 +26,7 @@ import { StreamManager } from "./stream_manager.js";
|
|||||||
export class BaseProtocol implements IBaseProtocol {
|
export class BaseProtocol implements IBaseProtocol {
|
||||||
public readonly addLibp2pEventListener: Libp2p["addEventListener"];
|
public readonly addLibp2pEventListener: Libp2p["addEventListener"];
|
||||||
public readonly removeLibp2pEventListener: Libp2p["removeEventListener"];
|
public readonly removeLibp2pEventListener: Libp2p["removeEventListener"];
|
||||||
|
readonly numPeersToUse: number;
|
||||||
protected streamManager: StreamManager;
|
protected streamManager: StreamManager;
|
||||||
protected pubsubTopics: PubsubTopic[];
|
protected pubsubTopics: PubsubTopic[];
|
||||||
|
|
||||||
@ -35,6 +38,8 @@ export class BaseProtocol implements IBaseProtocol {
|
|||||||
) {
|
) {
|
||||||
this.pubsubTopics = this.initializePubsubTopic(options);
|
this.pubsubTopics = this.initializePubsubTopic(options);
|
||||||
|
|
||||||
|
this.numPeersToUse = options?.numPeersToUse ?? DEFAULT_NUM_PEERS_TO_USE;
|
||||||
|
|
||||||
this.addLibp2pEventListener = components.events.addEventListener.bind(
|
this.addLibp2pEventListener = components.events.addEventListener.bind(
|
||||||
components.events
|
components.events
|
||||||
);
|
);
|
||||||
@ -124,6 +129,12 @@ export class BaseProtocol implements IBaseProtocol {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sortedFilteredPeers.length < numPeers) {
|
||||||
|
this.log.warn(
|
||||||
|
`Only ${sortedFilteredPeers.length} peers found. Requested ${numPeers}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return sortedFilteredPeers;
|
return sortedFilteredPeers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,13 +11,13 @@ import type {
|
|||||||
IProtoMessage,
|
IProtoMessage,
|
||||||
IReceiver,
|
IReceiver,
|
||||||
Libp2p,
|
Libp2p,
|
||||||
PeerIdStr,
|
|
||||||
ProtocolCreateOptions,
|
ProtocolCreateOptions,
|
||||||
PubsubTopic,
|
PubsubTopic,
|
||||||
SingleShardInfo,
|
SingleShardInfo,
|
||||||
Unsubscribe
|
Unsubscribe
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { DefaultPubsubTopic } from "@waku/interfaces";
|
import { DefaultPubsubTopic } from "@waku/interfaces";
|
||||||
|
import { messageHashStr } from "@waku/message-hash";
|
||||||
import { WakuMessage } from "@waku/proto";
|
import { WakuMessage } from "@waku/proto";
|
||||||
import {
|
import {
|
||||||
ensurePubsubTopicIsConfigured,
|
ensurePubsubTopicIsConfigured,
|
||||||
@ -50,10 +50,14 @@ export const FilterCodecs = {
|
|||||||
PUSH: "/vac/waku/filter-push/2.0.0-beta1"
|
PUSH: "/vac/waku/filter-push/2.0.0-beta1"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A subscription object refers to a subscription to a given pubsub topic.
|
||||||
|
*/
|
||||||
class Subscription {
|
class Subscription {
|
||||||
private readonly peer: Peer;
|
readonly peers: Peer[];
|
||||||
private readonly pubsubTopic: PubsubTopic;
|
private readonly pubsubTopic: PubsubTopic;
|
||||||
private newStream: (peer: Peer) => Promise<Stream>;
|
private newStream: (peer: Peer) => Promise<Stream>;
|
||||||
|
readonly receivedMessagesHashStr: string[] = [];
|
||||||
|
|
||||||
private subscriptionCallbacks: Map<
|
private subscriptionCallbacks: Map<
|
||||||
ContentTopic,
|
ContentTopic,
|
||||||
@ -62,10 +66,10 @@ class Subscription {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic,
|
||||||
remotePeer: Peer,
|
remotePeers: Peer[],
|
||||||
newStream: (peer: Peer) => Promise<Stream>
|
newStream: (peer: Peer) => Promise<Stream>
|
||||||
) {
|
) {
|
||||||
this.peer = remotePeer;
|
this.peers = remotePeers;
|
||||||
this.pubsubTopic = pubsubTopic;
|
this.pubsubTopic = pubsubTopic;
|
||||||
this.newStream = newStream;
|
this.newStream = newStream;
|
||||||
this.subscriptionCallbacks = new Map();
|
this.subscriptionCallbacks = new Map();
|
||||||
@ -89,53 +93,59 @@ class Subscription {
|
|||||||
const decodersGroupedByCT = groupByContentTopic(decodersArray);
|
const decodersGroupedByCT = groupByContentTopic(decodersArray);
|
||||||
const contentTopics = Array.from(decodersGroupedByCT.keys());
|
const contentTopics = Array.from(decodersGroupedByCT.keys());
|
||||||
|
|
||||||
const stream = await this.newStream(this.peer);
|
const promises = this.peers.map(async (peer) => {
|
||||||
|
const stream = await this.newStream(peer);
|
||||||
|
|
||||||
const request = FilterSubscribeRpc.createSubscribeRequest(
|
const request = FilterSubscribeRpc.createSubscribeRequest(
|
||||||
this.pubsubTopic,
|
this.pubsubTopic,
|
||||||
contentTopics
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await pipe(
|
|
||||||
[request.encode()],
|
|
||||||
lp.encode,
|
|
||||||
stream,
|
|
||||||
lp.decode,
|
|
||||||
async (source) => await all(source)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res || !res.length) {
|
|
||||||
throw Error(
|
|
||||||
`No response received for request ${request.requestId}: ${res}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { statusCode, requestId, statusDesc } =
|
|
||||||
FilterSubscribeResponse.decode(res[0].slice());
|
|
||||||
|
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
|
||||||
throw new Error(
|
|
||||||
`Filter subscribe request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Subscribed to peer ",
|
|
||||||
this.peer.id.toString(),
|
|
||||||
"for content topics",
|
|
||||||
contentTopics
|
contentTopics
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
throw new Error(
|
try {
|
||||||
"Error subscribing to peer: " +
|
const res = await pipe(
|
||||||
this.peer.id.toString() +
|
[request.encode()],
|
||||||
" for content topics: " +
|
lp.encode,
|
||||||
contentTopics +
|
stream,
|
||||||
": " +
|
lp.decode,
|
||||||
e
|
async (source) => await all(source)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
if (!res || !res.length) {
|
||||||
|
throw Error(
|
||||||
|
`No response received for request ${request.requestId}: ${res}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { statusCode, requestId, statusDesc } =
|
||||||
|
FilterSubscribeResponse.decode(res[0].slice());
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new Error(
|
||||||
|
`Filter subscribe request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Subscribed to peer ",
|
||||||
|
peer.id.toString(),
|
||||||
|
"for content topics",
|
||||||
|
contentTopics
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
"Error subscribing to peer: " +
|
||||||
|
peer.id.toString() +
|
||||||
|
" for content topics: " +
|
||||||
|
contentTopics +
|
||||||
|
": " +
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
this.handleErrors(results, "subscribe");
|
||||||
|
|
||||||
// Save the callback functions by content topics so they
|
// Save the callback functions by content topics so they
|
||||||
// can easily be removed (reciprocally replaced) if `unsubscribe` (reciprocally `subscribe`)
|
// can easily be removed (reciprocally replaced) if `unsubscribe` (reciprocally `subscribe`)
|
||||||
@ -155,125 +165,195 @@ class Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async unsubscribe(contentTopics: ContentTopic[]): Promise<void> {
|
async unsubscribe(contentTopics: ContentTopic[]): Promise<void> {
|
||||||
const stream = await this.newStream(this.peer);
|
const promises = this.peers.map(async (peer) => {
|
||||||
const unsubscribeRequest = FilterSubscribeRpc.createUnsubscribeRequest(
|
const stream = await this.newStream(peer);
|
||||||
this.pubsubTopic,
|
const unsubscribeRequest = FilterSubscribeRpc.createUnsubscribeRequest(
|
||||||
contentTopics
|
this.pubsubTopic,
|
||||||
);
|
contentTopics
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pipe([unsubscribeRequest.encode()], lp.encode, stream.sink);
|
await pipe([unsubscribeRequest.encode()], lp.encode, stream.sink);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("Error subscribing: " + error);
|
throw new Error("Error unsubscribing: " + error);
|
||||||
}
|
}
|
||||||
|
|
||||||
contentTopics.forEach((contentTopic: string) => {
|
contentTopics.forEach((contentTopic: string) => {
|
||||||
this.subscriptionCallbacks.delete(contentTopic);
|
this.subscriptionCallbacks.delete(contentTopic);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
this.handleErrors(results, "unsubscribe");
|
||||||
}
|
}
|
||||||
|
|
||||||
async ping(): Promise<void> {
|
async ping(): Promise<void> {
|
||||||
const stream = await this.newStream(this.peer);
|
const promises = this.peers.map(async (peer) => {
|
||||||
|
const stream = await this.newStream(peer);
|
||||||
|
|
||||||
const request = FilterSubscribeRpc.createSubscriberPingRequest();
|
const request = FilterSubscribeRpc.createSubscriberPingRequest();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await pipe(
|
const res = await pipe(
|
||||||
[request.encode()],
|
[request.encode()],
|
||||||
lp.encode,
|
lp.encode,
|
||||||
stream,
|
stream,
|
||||||
lp.decode,
|
lp.decode,
|
||||||
async (source) => await all(source)
|
async (source) => await all(source)
|
||||||
);
|
|
||||||
|
|
||||||
if (!res || !res.length) {
|
|
||||||
throw Error(
|
|
||||||
`No response received for request ${request.requestId}: ${res}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!res || !res.length) {
|
||||||
|
throw Error(
|
||||||
|
`No response received for request ${request.requestId}: ${res}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { statusCode, requestId, statusDesc } =
|
||||||
|
FilterSubscribeResponse.decode(res[0].slice());
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new Error(
|
||||||
|
`Filter ping request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.info(`Ping successful for peer ${peer.id.toString()}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error pinging: ", error);
|
||||||
|
throw error; // Rethrow the actual error instead of wrapping it
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const { statusCode, requestId, statusDesc } =
|
const results = await Promise.allSettled(promises);
|
||||||
FilterSubscribeResponse.decode(res[0].slice());
|
|
||||||
|
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
this.handleErrors(results, "ping");
|
||||||
throw new Error(
|
|
||||||
`Filter ping request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Ping successful");
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Error pinging: ", error);
|
|
||||||
throw new Error("Error pinging: " + error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unsubscribeAll(): Promise<void> {
|
async unsubscribeAll(): Promise<void> {
|
||||||
const stream = await this.newStream(this.peer);
|
const promises = this.peers.map(async (peer) => {
|
||||||
|
const stream = await this.newStream(peer);
|
||||||
|
|
||||||
const request = FilterSubscribeRpc.createUnsubscribeAllRequest(
|
const request = FilterSubscribeRpc.createUnsubscribeAllRequest(
|
||||||
this.pubsubTopic
|
this.pubsubTopic
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await pipe(
|
|
||||||
[request.encode()],
|
|
||||||
lp.encode,
|
|
||||||
stream,
|
|
||||||
lp.decode,
|
|
||||||
async (source) => await all(source)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res || !res.length) {
|
try {
|
||||||
throw Error(
|
const res = await pipe(
|
||||||
`No response received for request ${request.requestId}: ${res}`
|
[request.encode()],
|
||||||
|
lp.encode,
|
||||||
|
stream,
|
||||||
|
lp.decode,
|
||||||
|
async (source) => await all(source)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const { statusCode, requestId, statusDesc } =
|
if (!res || !res.length) {
|
||||||
FilterSubscribeResponse.decode(res[0].slice());
|
throw Error(
|
||||||
|
`No response received for request ${request.requestId}: ${res}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
const { statusCode, requestId, statusDesc } =
|
||||||
|
FilterSubscribeResponse.decode(res[0].slice());
|
||||||
|
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new Error(
|
||||||
|
`Filter unsubscribe all request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscriptionCallbacks.clear();
|
||||||
|
log.info(
|
||||||
|
`Unsubscribed from all content topics for pubsub topic ${this.pubsubTopic}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Filter unsubscribe all request ${requestId} failed with status code ${statusCode}: ${statusDesc}`
|
"Error unsubscribing from all content topics: " + error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.subscriptionCallbacks.clear();
|
const results = await Promise.allSettled(promises);
|
||||||
log.info("Unsubscribed from all content topics");
|
|
||||||
} catch (error) {
|
this.handleErrors(results, "unsubscribeAll");
|
||||||
throw new Error("Error unsubscribing from all content topics: " + error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async processMessage(message: WakuMessage): Promise<void> {
|
async processMessage(message: WakuMessage): Promise<void> {
|
||||||
const contentTopic = message.contentTopic;
|
const hashedMessageStr = messageHashStr(
|
||||||
|
this.pubsubTopic,
|
||||||
|
message as IProtoMessage
|
||||||
|
);
|
||||||
|
if (this.receivedMessagesHashStr.includes(hashedMessageStr)) {
|
||||||
|
log.info("Message already received, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.receivedMessagesHashStr.push(hashedMessageStr);
|
||||||
|
|
||||||
|
const { contentTopic } = message;
|
||||||
const subscriptionCallback = this.subscriptionCallbacks.get(contentTopic);
|
const subscriptionCallback = this.subscriptionCallbacks.get(contentTopic);
|
||||||
if (!subscriptionCallback) {
|
if (!subscriptionCallback) {
|
||||||
log.error("No subscription callback available for ", contentTopic);
|
log.error("No subscription callback available for ", contentTopic);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
log.info(
|
||||||
|
"Processing message with content topic ",
|
||||||
|
contentTopic,
|
||||||
|
" on pubsub topic ",
|
||||||
|
this.pubsubTopic
|
||||||
|
);
|
||||||
await pushMessage(subscriptionCallback, this.pubsubTopic, message);
|
await pushMessage(subscriptionCallback, this.pubsubTopic, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out only the rejected promises and extract & handle their reasons
|
||||||
|
private handleErrors(
|
||||||
|
results: PromiseSettledResult<void>[],
|
||||||
|
type: "ping" | "subscribe" | "unsubscribe" | "unsubscribeAll"
|
||||||
|
): void {
|
||||||
|
const errors = results
|
||||||
|
.filter(
|
||||||
|
(result): result is PromiseRejectedResult =>
|
||||||
|
result.status === "rejected"
|
||||||
|
)
|
||||||
|
.map((rejectedResult) => rejectedResult.reason);
|
||||||
|
|
||||||
|
if (errors.length === this.peers.length) {
|
||||||
|
const errorCounts = new Map<string, number>();
|
||||||
|
// TODO: streamline error logging with https://github.com/orgs/waku-org/projects/2/views/1?pane=issue&itemId=42849952
|
||||||
|
errors.forEach((error) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
errorCounts.set(message, (errorCounts.get(message) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueErrorMessages = Array.from(
|
||||||
|
errorCounts,
|
||||||
|
([message, count]) => `${message} (occurred ${count} times)`
|
||||||
|
).join(", ");
|
||||||
|
throw new Error(`Error ${type} all peers: ${uniqueErrorMessages}`);
|
||||||
|
} else if (errors.length > 0) {
|
||||||
|
// TODO: handle renewing faulty peers with new peers (https://github.com/waku-org/js-waku/issues/1463)
|
||||||
|
log.warn(
|
||||||
|
`Some ${type} failed. These will be refreshed with new peers`,
|
||||||
|
errors
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.info(`${type} successful for all peers`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Filter extends BaseProtocol implements IReceiver {
|
class Filter extends BaseProtocol implements IReceiver {
|
||||||
private activeSubscriptions = new Map<string, Subscription>();
|
private activeSubscriptions = new Map<string, Subscription>();
|
||||||
private readonly NUM_PEERS_PROTOCOL = 1;
|
|
||||||
|
|
||||||
private getActiveSubscription(
|
private getActiveSubscription(
|
||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic
|
||||||
peerIdStr: PeerIdStr
|
|
||||||
): Subscription | undefined {
|
): Subscription | undefined {
|
||||||
return this.activeSubscriptions.get(`${pubsubTopic}_${peerIdStr}`);
|
return this.activeSubscriptions.get(pubsubTopic);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setActiveSubscription(
|
private setActiveSubscription(
|
||||||
pubsubTopic: PubsubTopic,
|
pubsubTopic: PubsubTopic,
|
||||||
peerIdStr: PeerIdStr,
|
|
||||||
subscription: Subscription
|
subscription: Subscription
|
||||||
): Subscription {
|
): Subscription {
|
||||||
this.activeSubscriptions.set(`${pubsubTopic}_${peerIdStr}`, subscription);
|
this.activeSubscriptions.set(pubsubTopic, subscription);
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +367,12 @@ class Filter extends BaseProtocol implements IReceiver {
|
|||||||
this.activeSubscriptions = new Map();
|
this.activeSubscriptions = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new subscription to the given pubsub topic.
|
||||||
|
* The subscription is made to multiple peers for decentralization.
|
||||||
|
* @param pubsubTopicShardInfo The pubsub topic to subscribe to.
|
||||||
|
* @returns The subscription object.
|
||||||
|
*/
|
||||||
async createSubscription(
|
async createSubscription(
|
||||||
pubsubTopicShardInfo: SingleShardInfo | PubsubTopic = DefaultPubsubTopic
|
pubsubTopicShardInfo: SingleShardInfo | PubsubTopic = DefaultPubsubTopic
|
||||||
): Promise<Subscription> {
|
): Promise<Subscription> {
|
||||||
@ -297,23 +383,24 @@ class Filter extends BaseProtocol implements IReceiver {
|
|||||||
|
|
||||||
ensurePubsubTopicIsConfigured(pubsubTopic, this.pubsubTopics);
|
ensurePubsubTopicIsConfigured(pubsubTopic, this.pubsubTopics);
|
||||||
|
|
||||||
const peer = (
|
const peers = await this.getPeers({
|
||||||
await this.getPeers({
|
maxBootstrapPeers: 1,
|
||||||
maxBootstrapPeers: 1,
|
numPeers: this.numPeersToUse
|
||||||
numPeers: this.NUM_PEERS_PROTOCOL
|
});
|
||||||
})
|
if (peers.length === 0) {
|
||||||
)[0];
|
|
||||||
|
|
||||||
if (!peer) {
|
|
||||||
throw new Error("No peer found to initiate subscription.");
|
throw new Error("No peer found to initiate subscription.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Creating filter subscription with ${peers.length} peers: `,
|
||||||
|
peers.map((peer) => peer.id.toString())
|
||||||
|
);
|
||||||
|
|
||||||
const subscription =
|
const subscription =
|
||||||
this.getActiveSubscription(pubsubTopic, peer.id.toString()) ??
|
this.getActiveSubscription(pubsubTopic) ??
|
||||||
this.setActiveSubscription(
|
this.setActiveSubscription(
|
||||||
pubsubTopic,
|
pubsubTopic,
|
||||||
peer.id.toString(),
|
new Subscription(pubsubTopic, peers, this.getStream.bind(this))
|
||||||
new Subscription(pubsubTopic, peer, this.getStream.bind(this, peer))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
@ -360,8 +447,11 @@ class Filter extends BaseProtocol implements IReceiver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onRequest(streamData: IncomingStreamData): void {
|
private onRequest(streamData: IncomingStreamData): void {
|
||||||
|
const { connection, stream } = streamData;
|
||||||
|
const { remotePeer } = connection;
|
||||||
|
log.info(`Received message from ${remotePeer.toString()}`);
|
||||||
try {
|
try {
|
||||||
pipe(streamData.stream, lp.decode, async (source) => {
|
pipe(stream, lp.decode, async (source) => {
|
||||||
for await (const bytes of source) {
|
for await (const bytes of source) {
|
||||||
const response = FilterPushRpc.decode(bytes.slice());
|
const response = FilterPushRpc.decode(bytes.slice());
|
||||||
|
|
||||||
@ -377,11 +467,7 @@ class Filter extends BaseProtocol implements IReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const peerIdStr = streamData.connection.remotePeer.toString();
|
const subscription = this.getActiveSubscription(pubsubTopic);
|
||||||
const subscription = this.getActiveSubscription(
|
|
||||||
pubsubTopic,
|
|
||||||
peerIdStr
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
log.error(
|
log.error(
|
||||||
|
@ -42,8 +42,6 @@ type PreparePushMessageResult =
|
|||||||
* Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
|
* Implements the [Waku v2 Light Push protocol](https://rfc.vac.dev/spec/19/).
|
||||||
*/
|
*/
|
||||||
class LightPush extends BaseProtocol implements ILightPush {
|
class LightPush extends BaseProtocol implements ILightPush {
|
||||||
private readonly NUM_PEERS_PROTOCOL = 1;
|
|
||||||
|
|
||||||
constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
|
constructor(libp2p: Libp2p, options?: ProtocolCreateOptions) {
|
||||||
super(LightPushCodec, libp2p.components, log, options);
|
super(LightPushCodec, libp2p.components, log, options);
|
||||||
}
|
}
|
||||||
@ -106,7 +104,7 @@ class LightPush extends BaseProtocol implements ILightPush {
|
|||||||
|
|
||||||
const peers = await this.getPeers({
|
const peers = await this.getPeers({
|
||||||
maxBootstrapPeers: 1,
|
maxBootstrapPeers: 1,
|
||||||
numPeers: this.NUM_PEERS_PROTOCOL
|
numPeers: this.numPeersToUse
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!peers.length) {
|
if (!peers.length) {
|
||||||
@ -172,6 +170,8 @@ class LightPush extends BaseProtocol implements ILightPush {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
// TODO: handle renewing faulty peers with new peers (https://github.com/waku-org/js-waku/issues/1463)
|
||||||
const errors = results
|
const errors = results
|
||||||
.filter(
|
.filter(
|
||||||
(
|
(
|
||||||
|
@ -69,6 +69,14 @@ export type ProtocolCreateOptions = {
|
|||||||
* Notes that some values are overridden by {@link @waku/core!WakuNode} to ensure it implements the Waku protocol.
|
* Notes that some values are overridden by {@link @waku/core!WakuNode} to ensure it implements the Waku protocol.
|
||||||
*/
|
*/
|
||||||
libp2p?: Partial<CreateLibp2pOptions>;
|
libp2p?: Partial<CreateLibp2pOptions>;
|
||||||
|
/**
|
||||||
|
* Number of peers to connect to, for the usage of the protocol.
|
||||||
|
* This is used by:
|
||||||
|
* - Light Push to send messages,
|
||||||
|
* - Filter to retrieve messages.
|
||||||
|
* Defaults to 3.
|
||||||
|
*/
|
||||||
|
numPeersToUse?: number;
|
||||||
/**
|
/**
|
||||||
* Byte array used as key for the noise protocol used for connection encryption
|
* Byte array used as key for the noise protocol used for connection encryption
|
||||||
* by [`Libp2p.create`](https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#create)
|
* by [`Libp2p.create`](https://github.com/libp2p/js-libp2p/blob/master/doc/API.md#create)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { sha256 } from "@noble/hashes/sha256";
|
import { sha256 } from "@noble/hashes/sha256";
|
||||||
import type { IProtoMessage } from "@waku/interfaces";
|
import type { IProtoMessage } from "@waku/interfaces";
|
||||||
import { concat, utf8ToBytes } from "@waku/utils/bytes";
|
import { bytesToUtf8, concat, utf8ToBytes } from "@waku/utils/bytes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministic Message Hashing as defined in
|
* Deterministic Message Hashing as defined in
|
||||||
@ -27,3 +27,12 @@ export function messageHash(
|
|||||||
}
|
}
|
||||||
return sha256(bytes);
|
return sha256(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function messageHashStr(
|
||||||
|
pubsubTopic: string,
|
||||||
|
message: IProtoMessage
|
||||||
|
): string {
|
||||||
|
const hash = messageHash(pubsubTopic, message);
|
||||||
|
const hashStr = bytesToUtf8(hash);
|
||||||
|
return hashStr;
|
||||||
|
}
|
||||||
|
@ -1,2 +1,294 @@
|
|||||||
export * from "./message_collector.js";
|
import { DecodedMessage } from "@waku/core";
|
||||||
export * from "./service_node.js";
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
PubsubTopic,
|
||||||
|
ShardingParams
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { Args, MessageRpcQuery, MessageRpcResponse } from "../types";
|
||||||
|
import { delay, makeLogFileName } from "../utils/index.js";
|
||||||
|
|
||||||
|
import { MessageCollector } from "./message_collector.js";
|
||||||
|
import { defaultArgs, ServiceNode } from "./service_node.js";
|
||||||
|
|
||||||
|
export { ServiceNode, MessageCollector, defaultArgs };
|
||||||
|
|
||||||
|
const log = new Logger("test:message-collector");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is a wrapper over the ServiceNode & MessageCollector class
|
||||||
|
* that allows for the creation & handling of multiple ServiceNodes
|
||||||
|
*/
|
||||||
|
export class ServiceNodesFleet {
|
||||||
|
static async createAndRun(
|
||||||
|
mochaContext: Mocha.Context,
|
||||||
|
pubsubTopics: PubsubTopic[],
|
||||||
|
nodesToCreate: number = 3,
|
||||||
|
strictChecking: boolean = false,
|
||||||
|
shardInfo?: ShardingParams,
|
||||||
|
_args?: Args,
|
||||||
|
withoutFilter = false
|
||||||
|
): Promise<ServiceNodesFleet> {
|
||||||
|
const serviceNodePromises = Array.from(
|
||||||
|
{ length: nodesToCreate },
|
||||||
|
async () => {
|
||||||
|
const node = new ServiceNode(
|
||||||
|
makeLogFileName(mochaContext) +
|
||||||
|
Math.random().toString(36).substring(7)
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = getArgs(pubsubTopics, shardInfo, _args);
|
||||||
|
await node.start(args, {
|
||||||
|
retries: 3
|
||||||
|
});
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodes = await Promise.all(serviceNodePromises);
|
||||||
|
return new ServiceNodesFleet(nodes, withoutFilter, strictChecking);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a [[WakuMessage]] to a [[WakuRelayMessage]]. The latter is used
|
||||||
|
* by the nwaku JSON-RPC API.
|
||||||
|
*/
|
||||||
|
static toMessageRpcQuery(message: {
|
||||||
|
payload: Uint8Array;
|
||||||
|
contentTopic: string;
|
||||||
|
timestamp?: Date;
|
||||||
|
}): MessageRpcQuery {
|
||||||
|
return ServiceNode.toMessageRpcQuery(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public messageCollector: MultipleNodesMessageCollector;
|
||||||
|
private constructor(
|
||||||
|
public nodes: ServiceNode[],
|
||||||
|
relay: boolean,
|
||||||
|
private strictChecking: boolean
|
||||||
|
) {
|
||||||
|
const _messageCollectors: MessageCollector[] = [];
|
||||||
|
this.nodes.forEach((node) => {
|
||||||
|
_messageCollectors.push(new MessageCollector(node));
|
||||||
|
});
|
||||||
|
this.messageCollector = new MultipleNodesMessageCollector(
|
||||||
|
_messageCollectors,
|
||||||
|
relay ? this.nodes : undefined,
|
||||||
|
strictChecking
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): "go-waku" | "nwaku" {
|
||||||
|
const nodeType = new Set(
|
||||||
|
this.nodes.map((node) => {
|
||||||
|
return node.type();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (nodeType.size > 1) {
|
||||||
|
throw new Error("Multiple node types");
|
||||||
|
}
|
||||||
|
return nodeType.values().next().value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const startPromises = this.nodes.map((node) => node.start());
|
||||||
|
await Promise.all(startPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendRelayMessage(
|
||||||
|
message: MessageRpcQuery,
|
||||||
|
pubsubTopic?: string,
|
||||||
|
raw = false
|
||||||
|
): Promise<boolean> {
|
||||||
|
let relayMessagePromises: Promise<boolean>[];
|
||||||
|
if (raw) {
|
||||||
|
relayMessagePromises = this.nodes.map((node) =>
|
||||||
|
node.rpcCall<boolean>("post_waku_v2_relay_v1_message", [
|
||||||
|
pubsubTopic && pubsubTopic,
|
||||||
|
message
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
relayMessagePromises = this.nodes.map((node) =>
|
||||||
|
node.sendMessage(message, pubsubTopic)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const relayMessages = await Promise.all(relayMessagePromises);
|
||||||
|
return relayMessages.every((message) => message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmMessageLength(numMessages: number): Promise<void> {
|
||||||
|
if (this.strictChecking) {
|
||||||
|
await Promise.all(
|
||||||
|
this.nodes.map(async (node) =>
|
||||||
|
expect(await node.messages()).to.have.length(numMessages)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Wait for all promises to resolve and check if any meets the condition
|
||||||
|
const results = await Promise.all(
|
||||||
|
this.nodes.map(async (node) => {
|
||||||
|
const msgs = await node.messages();
|
||||||
|
return msgs.length === numMessages;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if at least one result meets the condition
|
||||||
|
const conditionMet = results.some((result) => result);
|
||||||
|
expect(conditionMet).to.be.true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultipleNodesMessageCollector {
|
||||||
|
callback: (msg: DecodedMessage) => void = () => {};
|
||||||
|
messageList: Array<DecodedMessage> = [];
|
||||||
|
constructor(
|
||||||
|
private messageCollectors: MessageCollector[],
|
||||||
|
private relayNodes?: ServiceNode[],
|
||||||
|
private strictChecking: boolean = false
|
||||||
|
) {
|
||||||
|
this.callback = (msg: DecodedMessage): void => {
|
||||||
|
log.info("Got a message");
|
||||||
|
this.messageList.push(msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get count(): number {
|
||||||
|
return this.messageList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasMessage(topic: string, text: string): boolean {
|
||||||
|
if (this.strictChecking) {
|
||||||
|
return this.messageCollectors.every((collector) =>
|
||||||
|
collector.hasMessage(topic, text)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.messageCollectors.some((collector) =>
|
||||||
|
collector.hasMessage(topic, text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMessage(index: number): MessageRpcResponse | DecodedMessage {
|
||||||
|
return this.messageList[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a received message against expected values on all nodes.
|
||||||
|
* Returns true if any node's collector verifies the message successfully.
|
||||||
|
*/
|
||||||
|
verifyReceivedMessage(
|
||||||
|
index: number,
|
||||||
|
options: {
|
||||||
|
expectedMessageText: string | Uint8Array | undefined;
|
||||||
|
expectedContentTopic?: string;
|
||||||
|
expectedPubsubTopic?: string;
|
||||||
|
expectedVersion?: number;
|
||||||
|
expectedMeta?: Uint8Array;
|
||||||
|
expectedEphemeral?: boolean;
|
||||||
|
expectedTimestamp?: bigint | number;
|
||||||
|
checkTimestamp?: boolean;
|
||||||
|
}
|
||||||
|
): boolean {
|
||||||
|
if (this.strictChecking) {
|
||||||
|
return this.messageCollectors.every((collector) => {
|
||||||
|
try {
|
||||||
|
collector.verifyReceivedMessage(index, options);
|
||||||
|
return true; // Verification successful
|
||||||
|
} catch (error) {
|
||||||
|
return false; // Verification failed, continue with the next collector
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.messageCollectors.some((collector) => {
|
||||||
|
try {
|
||||||
|
collector.verifyReceivedMessage(index, options);
|
||||||
|
return true; // Verification successful
|
||||||
|
} catch (error) {
|
||||||
|
return false; // Verification failed, continue with the next collector
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a total number of messages across all nodes.
|
||||||
|
*/
|
||||||
|
async waitForMessages(
|
||||||
|
numMessages: number,
|
||||||
|
options?: {
|
||||||
|
pubsubTopic?: string;
|
||||||
|
timeoutDuration?: number;
|
||||||
|
exact?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const pubsubTopic = options?.pubsubTopic || DefaultPubsubTopic;
|
||||||
|
const timeoutDuration = options?.timeoutDuration || 400;
|
||||||
|
const exact = options?.exact || false;
|
||||||
|
|
||||||
|
while (this.messageList.length < numMessages) {
|
||||||
|
if (this.relayNodes) {
|
||||||
|
if (this.strictChecking) {
|
||||||
|
const results = await Promise.all(
|
||||||
|
this.relayNodes.map(async (node) => {
|
||||||
|
const msgs = await node.messages(pubsubTopic);
|
||||||
|
return msgs.length >= numMessages;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return results.every((result) => result);
|
||||||
|
} else {
|
||||||
|
const results = await Promise.all(
|
||||||
|
this.relayNodes.map(async (node) => {
|
||||||
|
const msgs = await node.messages(pubsubTopic);
|
||||||
|
return msgs.length >= numMessages;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return results.some((result) => result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - startTime > timeoutDuration * numMessages) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exact) {
|
||||||
|
if (this.messageList.length == numMessages) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`Was expecting exactly ${numMessages} messages. Received: ${this.messageList.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArgs(
|
||||||
|
pubsubTopics: PubsubTopic[],
|
||||||
|
shardInfo?: ShardingParams,
|
||||||
|
args?: Args
|
||||||
|
): Args {
|
||||||
|
const defaultArgs = {
|
||||||
|
lightpush: true,
|
||||||
|
filter: true,
|
||||||
|
discv5Discovery: true,
|
||||||
|
peerExchange: true,
|
||||||
|
relay: true,
|
||||||
|
pubsubTopic: pubsubTopics,
|
||||||
|
...(shardInfo && { clusterId: shardInfo.clusterId })
|
||||||
|
} as Args;
|
||||||
|
|
||||||
|
return { ...defaultArgs, ...args };
|
||||||
|
}
|
||||||
|
@ -5,8 +5,11 @@ import { bytesToUtf8, utf8ToBytes } from "@waku/utils/bytes";
|
|||||||
import { AssertionError, expect } from "chai";
|
import { AssertionError, expect } from "chai";
|
||||||
import { equals } from "uint8arrays/equals";
|
import { equals } from "uint8arrays/equals";
|
||||||
|
|
||||||
import { base64ToUtf8, delay, ServiceNode } from "../index.js";
|
|
||||||
import { MessageRpcResponse } from "../types.js";
|
import { MessageRpcResponse } from "../types.js";
|
||||||
|
import { base64ToUtf8 } from "../utils/base64_utf8.js";
|
||||||
|
import { delay } from "../utils/delay.js";
|
||||||
|
|
||||||
|
import { ServiceNode } from "./service_node.js";
|
||||||
|
|
||||||
const log = new Logger("test:message-collector");
|
const log = new Logger("test:message-collector");
|
||||||
|
|
||||||
|
@ -4,3 +4,4 @@ export * from "./random_array.js";
|
|||||||
export * from "./wait_for_remote_peer_with_codec.js";
|
export * from "./wait_for_remote_peer_with_codec.js";
|
||||||
export * from "./delay.js";
|
export * from "./delay.js";
|
||||||
export * from "./base64_utf8.js";
|
export * from "./base64_utf8.js";
|
||||||
|
export * from "./waitForConnections.js";
|
||||||
|
20
packages/tests/src/utils/waitForConnections.ts
Normal file
20
packages/tests/src/utils/waitForConnections.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { LightNode } from "@waku/interfaces";
|
||||||
|
export async function waitForConnections(
|
||||||
|
numPeers: number,
|
||||||
|
waku: LightNode
|
||||||
|
): Promise<void> {
|
||||||
|
let connectionsLen = waku.libp2p.getConnections().length;
|
||||||
|
if (connectionsLen >= numPeers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const cb = (): void => {
|
||||||
|
connectionsLen++;
|
||||||
|
if (connectionsLen >= numPeers) {
|
||||||
|
waku.libp2p.removeEventListener("peer:identify", cb);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waku.libp2p.addEventListener("peer:identify", cb);
|
||||||
|
});
|
||||||
|
}
|
20
packages/tests/src/waitForConnections.ts
Normal file
20
packages/tests/src/waitForConnections.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { LightNode } from "@waku/interfaces";
|
||||||
|
export async function waitForConnections(
|
||||||
|
numPeers: number,
|
||||||
|
waku: LightNode
|
||||||
|
): Promise<void> {
|
||||||
|
let connectionsLen = waku.libp2p.getConnections().length;
|
||||||
|
if (connectionsLen >= numPeers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const cb = (): void => {
|
||||||
|
connectionsLen++;
|
||||||
|
if (connectionsLen >= numPeers) {
|
||||||
|
waku.libp2p.removeEventListener("peer:identify", cb);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waku.libp2p.addEventListener("peer:identify", cb);
|
||||||
|
});
|
||||||
|
}
|
117
packages/tests/tests/filter/ping.node.spec.ts
Normal file
117
packages/tests/tests/filter/ping.node.spec.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
IFilterSubscription,
|
||||||
|
LightNode
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { ServiceNodesFleet } from "../../src/index.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
runMultipleNodes,
|
||||||
|
teardownNodesWithRedundancy,
|
||||||
|
TestContentTopic,
|
||||||
|
TestDecoder,
|
||||||
|
TestEncoder,
|
||||||
|
validatePingError
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
const runTests = (strictCheckNodes: boolean): void => {
|
||||||
|
describe(`Waku Filter V2: Ping: Multiple Nodes: Strict Checking: ${strictCheckNodes}`, function () {
|
||||||
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
|
this.timeout(10000);
|
||||||
|
let waku: LightNode;
|
||||||
|
let serviceNodes: ServiceNodesFleet;
|
||||||
|
let subscription: IFilterSubscription;
|
||||||
|
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
[serviceNodes, waku] = await runMultipleNodes(this, [DefaultPubsubTopic]);
|
||||||
|
subscription = await waku.filter.createSubscription();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ping on subscribed peer", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// If ping is successfull(node has active subscription) we receive a success status code.
|
||||||
|
await subscription.ping();
|
||||||
|
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
|
||||||
|
// Confirm new messages are received after a ping.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ping on peer without subscriptions", async function () {
|
||||||
|
await validatePingError(subscription);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Ping on unsubscribed peer", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await subscription.ping();
|
||||||
|
await subscription.unsubscribe([TestContentTopic]);
|
||||||
|
|
||||||
|
// Ping imediately after unsubscribe
|
||||||
|
await validatePingError(subscription);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Reopen subscription with peer with lost subscription", async function () {
|
||||||
|
const openSubscription = async (): Promise<void> => {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribe = async (): Promise<void> => {
|
||||||
|
await subscription.unsubscribe([TestContentTopic]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pingAndReinitiateSubscription = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await subscription.ping();
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("peer has no subscriptions")
|
||||||
|
) {
|
||||||
|
await openSubscription();
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// open subscription & ping -> should pass
|
||||||
|
await openSubscription();
|
||||||
|
await pingAndReinitiateSubscription();
|
||||||
|
|
||||||
|
// unsubscribe & ping -> should fail and reinitiate subscription
|
||||||
|
await unsubscribe();
|
||||||
|
await pingAndReinitiateSubscription();
|
||||||
|
|
||||||
|
// ping -> should pass as subscription is reinitiated
|
||||||
|
await pingAndReinitiateSubscription();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[true, false].map(runTests);
|
321
packages/tests/tests/filter/push.node.spec.ts
Normal file
321
packages/tests/tests/filter/push.node.spec.ts
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
import { waitForRemotePeer } from "@waku/core";
|
||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
IFilterSubscription,
|
||||||
|
LightNode,
|
||||||
|
Protocols
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
delay,
|
||||||
|
ServiceNodesFleet,
|
||||||
|
TEST_STRING,
|
||||||
|
TEST_TIMESTAMPS
|
||||||
|
} from "../../src/index.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
messageText,
|
||||||
|
runMultipleNodes,
|
||||||
|
teardownNodesWithRedundancy,
|
||||||
|
TestContentTopic,
|
||||||
|
TestDecoder,
|
||||||
|
TestEncoder
|
||||||
|
} from "./utils.js";
|
||||||
|
|
||||||
|
const runTests = (strictCheckNodes: boolean): void => {
|
||||||
|
describe(`Waku Filter V2: FilterPush: Multiple Nodes: Strict Checking: ${strictCheckNodes}`, function () {
|
||||||
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
|
this.timeout(10000);
|
||||||
|
let waku: LightNode;
|
||||||
|
let serviceNodes: ServiceNodesFleet;
|
||||||
|
let subscription: IFilterSubscription;
|
||||||
|
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
[serviceNodes, waku] = await runMultipleNodes(this, [DefaultPubsubTopic]);
|
||||||
|
subscription = await waku.filter.createSubscription();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST_STRING.forEach((testItem) => {
|
||||||
|
it(`Check received message containing ${testItem.description}`, async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(testItem.value)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: testItem.value,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST_TIMESTAMPS.forEach((testItem) => {
|
||||||
|
it(`Check received message with timestamp: ${testItem} `, async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: Buffer.from(utf8ToBytes(messageText)).toString("base64"),
|
||||||
|
timestamp: testItem as any
|
||||||
|
},
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
checkTimestamp: false,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the timestamp matches
|
||||||
|
const timestamp = serviceNodes.messageCollector.getMessage(0).timestamp;
|
||||||
|
if (testItem == undefined) {
|
||||||
|
expect(timestamp).to.eq(undefined);
|
||||||
|
}
|
||||||
|
if (timestamp !== undefined && timestamp instanceof Date) {
|
||||||
|
expect(testItem?.toString()).to.contain(
|
||||||
|
timestamp.getTime().toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message with invalid timestamp is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: Buffer.from(utf8ToBytes(messageText)).toString("base64"),
|
||||||
|
timestamp: "2023-09-06T12:05:38.609Z" as any
|
||||||
|
},
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that no message was received
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message on other pubsub topic is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: Buffer.from(utf8ToBytes(messageText)).toString("base64"),
|
||||||
|
timestamp: BigInt(Date.now()) * BigInt(1000000)
|
||||||
|
},
|
||||||
|
"DefaultPubsubTopic"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message with no pubsub topic is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: Buffer.from(utf8ToBytes(messageText)).toString("base64"),
|
||||||
|
timestamp: BigInt(Date.now()) * BigInt(1000000)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message with no content topic is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
payload: Buffer.from(utf8ToBytes(messageText)).toString("base64"),
|
||||||
|
timestamp: BigInt(Date.now()) * BigInt(1000000)
|
||||||
|
},
|
||||||
|
DefaultPubsubTopic
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message with no payload is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
timestamp: BigInt(Date.now()) * BigInt(1000000),
|
||||||
|
payload: undefined as any
|
||||||
|
},
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// For go-waku the message is received (it is possible to send a message with no payload)
|
||||||
|
if (serviceNodes.type == "go-waku") {
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Check message with non string payload is not received", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
await serviceNodes.sendRelayMessage(
|
||||||
|
{
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: 12345 as unknown as string,
|
||||||
|
timestamp: BigInt(Date.now()) * BigInt(1000000)
|
||||||
|
},
|
||||||
|
DefaultPubsubTopic
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Will be skipped until https://github.com/waku-org/js-waku/issues/1464 si done
|
||||||
|
it.skip("Check message received after jswaku node is restarted", async function () {
|
||||||
|
// Subscribe and send message
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restart js-waku node
|
||||||
|
await waku.stop();
|
||||||
|
expect(waku.isStarted()).to.eq(false);
|
||||||
|
await waku.start();
|
||||||
|
expect(waku.isStarted()).to.eq(true);
|
||||||
|
|
||||||
|
// Redo the connection and create a new subscription
|
||||||
|
for (const node of this.serviceNodes) {
|
||||||
|
await waku.dial(await node.getMultiaddrWithId());
|
||||||
|
await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush]);
|
||||||
|
}
|
||||||
|
subscription = await waku.filter.createSubscription();
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
|
||||||
|
// Confirm both messages were received.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: "M1",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedMessageText: "M2",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Will be skipped until https://github.com/waku-org/js-waku/issues/1464 si done
|
||||||
|
it.skip("Check message received after nwaku node is restarted", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restart nwaku node
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, []);
|
||||||
|
await serviceNodes.start();
|
||||||
|
await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush]);
|
||||||
|
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
|
||||||
|
// Confirm both messages were received.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: "M1",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedMessageText: "M2",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[true, false].map(runTests);
|
@ -21,7 +21,8 @@ import {
|
|||||||
ServiceNode,
|
ServiceNode,
|
||||||
tearDownNodes
|
tearDownNodes
|
||||||
} from "../../../src/index.js";
|
} from "../../../src/index.js";
|
||||||
import { runNodes } from "../utils.js";
|
|
||||||
|
import { runNodes } from "./utils.js";
|
||||||
|
|
||||||
describe("Waku Filter V2: Multiple PubsubTopics", function () {
|
describe("Waku Filter V2: Multiple PubsubTopics", function () {
|
||||||
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { DefaultPubsubTopic } from "@waku/interfaces";
|
import {
|
||||||
import type { IFilterSubscription, LightNode } from "@waku/interfaces";
|
DefaultPubsubTopic,
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
IFilterSubscription,
|
||||||
|
LightNode
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -9,13 +12,14 @@ import {
|
|||||||
tearDownNodes
|
tearDownNodes
|
||||||
} from "../../../src/index.js";
|
} from "../../../src/index.js";
|
||||||
import {
|
import {
|
||||||
runNodes,
|
|
||||||
TestContentTopic,
|
TestContentTopic,
|
||||||
TestDecoder,
|
TestDecoder,
|
||||||
TestEncoder,
|
TestEncoder,
|
||||||
validatePingError
|
validatePingError
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
|
|
||||||
|
import { runNodes } from "./utils.js";
|
||||||
|
|
||||||
describe("Waku Filter V2: Ping", function () {
|
describe("Waku Filter V2: Ping", function () {
|
||||||
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { waitForRemotePeer } from "@waku/core";
|
import { waitForRemotePeer } from "@waku/core";
|
||||||
import type { IFilterSubscription, LightNode } from "@waku/interfaces";
|
import {
|
||||||
import { DefaultPubsubTopic } from "@waku/interfaces";
|
DefaultPubsubTopic,
|
||||||
import { Protocols } from "@waku/interfaces";
|
IFilterSubscription,
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
LightNode,
|
||||||
|
Protocols
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -13,9 +16,9 @@ import {
|
|||||||
TEST_STRING,
|
TEST_STRING,
|
||||||
TEST_TIMESTAMPS
|
TEST_TIMESTAMPS
|
||||||
} from "../../../src/index.js";
|
} from "../../../src/index.js";
|
||||||
|
import { runNodes } from "../../light-push/utils";
|
||||||
import {
|
import {
|
||||||
messageText,
|
messageText,
|
||||||
runNodes,
|
|
||||||
TestContentTopic,
|
TestContentTopic,
|
||||||
TestDecoder,
|
TestDecoder,
|
||||||
TestEncoder
|
TestEncoder
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { createDecoder, createEncoder, waitForRemotePeer } from "@waku/core";
|
import { createDecoder, createEncoder, waitForRemotePeer } from "@waku/core";
|
||||||
import type { IFilterSubscription, LightNode } from "@waku/interfaces";
|
import {
|
||||||
import { DefaultPubsubTopic } from "@waku/interfaces";
|
DefaultPubsubTopic,
|
||||||
import { Protocols } from "@waku/interfaces";
|
IFilterSubscription,
|
||||||
|
LightNode,
|
||||||
|
Protocols
|
||||||
|
} from "@waku/interfaces";
|
||||||
import {
|
import {
|
||||||
ecies,
|
ecies,
|
||||||
generatePrivateKey,
|
generatePrivateKey,
|
||||||
@ -9,7 +12,7 @@ import {
|
|||||||
getPublicKey,
|
getPublicKey,
|
||||||
symmetric
|
symmetric
|
||||||
} from "@waku/message-encryption";
|
} from "@waku/message-encryption";
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -25,13 +28,14 @@ import {
|
|||||||
import {
|
import {
|
||||||
messagePayload,
|
messagePayload,
|
||||||
messageText,
|
messageText,
|
||||||
runNodes,
|
|
||||||
TestContentTopic,
|
TestContentTopic,
|
||||||
TestDecoder,
|
TestDecoder,
|
||||||
TestEncoder
|
TestEncoder
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
|
|
||||||
describe("Waku Filter V2: Subscribe", function () {
|
import { runNodes } from "./utils.js";
|
||||||
|
|
||||||
|
describe("Waku Filter V2: Subscribe: Single Service Node", function () {
|
||||||
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
let waku: LightNode;
|
let waku: LightNode;
|
||||||
@ -371,7 +375,7 @@ describe("Waku Filter V2: Subscribe", function () {
|
|||||||
|
|
||||||
// Send messages to the first set of topics.
|
// Send messages to the first set of topics.
|
||||||
for (let i = 0; i < topicCount1; i++) {
|
for (let i = 0; i < topicCount1; i++) {
|
||||||
const messageText = `Message for Topic ${i + 1}`;
|
const messageText = `Topic Set 1: Message Number: ${i + 1}`;
|
||||||
await waku.lightPush.send(td1.encoders[i], {
|
await waku.lightPush.send(td1.encoders[i], {
|
||||||
payload: utf8ToBytes(messageText)
|
payload: utf8ToBytes(messageText)
|
||||||
});
|
});
|
||||||
@ -379,7 +383,8 @@ describe("Waku Filter V2: Subscribe", function () {
|
|||||||
|
|
||||||
// Send messages to the second set of topics.
|
// Send messages to the second set of topics.
|
||||||
for (let i = 0; i < topicCount2; i++) {
|
for (let i = 0; i < topicCount2; i++) {
|
||||||
const messageText = `Message for Topic ${i + 1}`;
|
const messageText = `Topic Set 2: Message Number: ${i + 1}`;
|
||||||
|
|
||||||
await waku.lightPush.send(td2.encoders[i], {
|
await waku.lightPush.send(td2.encoders[i], {
|
||||||
payload: utf8ToBytes(messageText)
|
payload: utf8ToBytes(messageText)
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { createDecoder, createEncoder } from "@waku/core";
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
import type { IFilterSubscription, LightNode } from "@waku/interfaces";
|
import { DefaultPubsubTopic, IFilterSubscription } from "@waku/interfaces";
|
||||||
import { DefaultPubsubTopic } from "@waku/interfaces";
|
import { LightNode } from "@waku/interfaces";
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,10 +10,10 @@ import {
|
|||||||
ServiceNode,
|
ServiceNode,
|
||||||
tearDownNodes
|
tearDownNodes
|
||||||
} from "../../../src/index.js";
|
} from "../../../src/index.js";
|
||||||
|
import { runNodes } from "../../light-push/utils";
|
||||||
import {
|
import {
|
||||||
messagePayload,
|
messagePayload,
|
||||||
messageText,
|
messageText,
|
||||||
runNodes,
|
|
||||||
TestContentTopic,
|
TestContentTopic,
|
||||||
TestDecoder,
|
TestDecoder,
|
||||||
TestEncoder
|
TestEncoder
|
||||||
|
67
packages/tests/tests/filter/single_node/utils.ts
Normal file
67
packages/tests/tests/filter/single_node/utils.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { waitForRemotePeer } from "@waku/core";
|
||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
LightNode,
|
||||||
|
ProtocolCreateOptions,
|
||||||
|
Protocols,
|
||||||
|
ShardingParams
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { createLightNode } from "@waku/sdk";
|
||||||
|
import { Logger } from "@waku/utils";
|
||||||
|
import { Context } from "mocha";
|
||||||
|
|
||||||
|
import {
|
||||||
|
makeLogFileName,
|
||||||
|
NOISE_KEY_1,
|
||||||
|
ServiceNode
|
||||||
|
} from "../../../src/index.js";
|
||||||
|
|
||||||
|
export const log = new Logger("test:filter:single_node");
|
||||||
|
|
||||||
|
export async function runNodes(
|
||||||
|
context: Context,
|
||||||
|
//TODO: change this to use `ShardInfo` instead of `string[]`
|
||||||
|
pubsubTopics: string[],
|
||||||
|
shardInfo?: ShardingParams
|
||||||
|
): Promise<[ServiceNode, LightNode]> {
|
||||||
|
const nwaku = new ServiceNode(makeLogFileName(context));
|
||||||
|
|
||||||
|
await nwaku.start(
|
||||||
|
{
|
||||||
|
filter: true,
|
||||||
|
lightpush: true,
|
||||||
|
relay: true,
|
||||||
|
pubsubTopic: pubsubTopics,
|
||||||
|
...(shardInfo && { clusterId: shardInfo.clusterId })
|
||||||
|
},
|
||||||
|
{ retries: 3 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const waku_options: ProtocolCreateOptions = {
|
||||||
|
staticNoiseKey: NOISE_KEY_1,
|
||||||
|
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } },
|
||||||
|
pubsubTopics: shardInfo ? undefined : pubsubTopics,
|
||||||
|
...((pubsubTopics.length !== 1 ||
|
||||||
|
pubsubTopics[0] !== DefaultPubsubTopic) && {
|
||||||
|
shardInfo: shardInfo
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info("Starting js waku node with :", JSON.stringify(waku_options));
|
||||||
|
let waku: LightNode | undefined;
|
||||||
|
try {
|
||||||
|
waku = await createLightNode(waku_options);
|
||||||
|
await waku.start();
|
||||||
|
} catch (error) {
|
||||||
|
log.error("jswaku node failed to start:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waku) {
|
||||||
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
|
await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush]);
|
||||||
|
await nwaku.ensureSubscriptions(pubsubTopics);
|
||||||
|
return [nwaku, waku];
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to initialize waku");
|
||||||
|
}
|
||||||
|
}
|
453
packages/tests/tests/filter/subscribe.node.spec.ts
Normal file
453
packages/tests/tests/filter/subscribe.node.spec.ts
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
IFilterSubscription,
|
||||||
|
LightNode
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import {
|
||||||
|
ecies,
|
||||||
|
generatePrivateKey,
|
||||||
|
generateSymmetricKey,
|
||||||
|
getPublicKey,
|
||||||
|
symmetric
|
||||||
|
} from "@waku/message-encryption";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
delay,
|
||||||
|
generateTestData,
|
||||||
|
ServiceNodesFleet,
|
||||||
|
TEST_STRING
|
||||||
|
} from "../../src/index.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
messagePayload,
|
||||||
|
messageText,
|
||||||
|
runMultipleNodes,
|
||||||
|
teardownNodesWithRedundancy,
|
||||||
|
TestContentTopic,
|
||||||
|
TestDecoder,
|
||||||
|
TestEncoder
|
||||||
|
} from "./utils.js";
|
||||||
|
|
||||||
|
const runTests = (strictCheckNodes: boolean): void => {
|
||||||
|
describe(`Waku Filter V2: Subscribe: Multiple Service Nodes: Strict Check mode: ${strictCheckNodes}`, function () {
|
||||||
|
this.timeout(100000);
|
||||||
|
let waku: LightNode;
|
||||||
|
let serviceNodes: ServiceNodesFleet;
|
||||||
|
let subscription: IFilterSubscription;
|
||||||
|
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
[serviceNodes, waku] = await runMultipleNodes(
|
||||||
|
this,
|
||||||
|
[DefaultPubsubTopic],
|
||||||
|
strictCheckNodes
|
||||||
|
);
|
||||||
|
subscription = await waku.filter.createSubscription();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive messages via lightPush", async function () {
|
||||||
|
expect(waku.libp2p.getConnections()).has.length(3);
|
||||||
|
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(TestEncoder, messagePayload);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive ecies encrypted messages via lightPush", async function () {
|
||||||
|
const privateKey = generatePrivateKey();
|
||||||
|
const publicKey = getPublicKey(privateKey);
|
||||||
|
const encoder = ecies.createEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
publicKey
|
||||||
|
});
|
||||||
|
const decoder = ecies.createDecoder(TestContentTopic, privateKey);
|
||||||
|
|
||||||
|
await subscription.subscribe(
|
||||||
|
[decoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(encoder, messagePayload);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic,
|
||||||
|
expectedVersion: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive symmetrically encrypted messages via lightPush", async function () {
|
||||||
|
const symKey = generateSymmetricKey();
|
||||||
|
const encoder = symmetric.createEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
symKey
|
||||||
|
});
|
||||||
|
const decoder = symmetric.createDecoder(TestContentTopic, symKey);
|
||||||
|
|
||||||
|
await subscription.subscribe(
|
||||||
|
[decoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(encoder, messagePayload);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic,
|
||||||
|
expectedVersion: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive messages via waku relay post", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await delay(400);
|
||||||
|
|
||||||
|
// Send a test message using the relay post method.
|
||||||
|
const relayMessage = ServiceNodesFleet.toMessageRpcQuery({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
payload: utf8ToBytes(messageText)
|
||||||
|
});
|
||||||
|
await serviceNodes.sendRelayMessage(relayMessage);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive 2 messages on the same topic", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(TestEncoder, messagePayload);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send another message on the same topic.
|
||||||
|
const newMessageText = "Filtering still works!";
|
||||||
|
await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(newMessageText)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the second message was successfully received.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedMessageText: newMessageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receive messages on 2 different content topics", async function () {
|
||||||
|
// Subscribe to the first content topic and send a message.
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, messagePayload);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modify subscription to include a new content topic and send a message.
|
||||||
|
const newMessageText = "Filtering still works!";
|
||||||
|
const newMessagePayload = { payload: utf8ToBytes(newMessageText) };
|
||||||
|
const newContentTopic = "/test/2/waku-filter";
|
||||||
|
const newEncoder = createEncoder({ contentTopic: newContentTopic });
|
||||||
|
const newDecoder = createDecoder(newContentTopic);
|
||||||
|
await subscription.subscribe(
|
||||||
|
[newDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(newEncoder, {
|
||||||
|
payload: utf8ToBytes(newMessageText)
|
||||||
|
});
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedContentTopic: newContentTopic,
|
||||||
|
expectedMessageText: newMessageText
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send another message on the initial content topic to verify it still works.
|
||||||
|
await waku.lightPush.send(TestEncoder, newMessagePayload);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(3)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(2, {
|
||||||
|
expectedMessageText: newMessageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe and receives messages on 20 topics", async function () {
|
||||||
|
const topicCount = 20;
|
||||||
|
const td = generateTestData(topicCount);
|
||||||
|
|
||||||
|
// Subscribe to all 20 topics.
|
||||||
|
for (let i = 0; i < topicCount; i++) {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[td.decoders[i]],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a unique message on each topic.
|
||||||
|
for (let i = 0; i < topicCount; i++) {
|
||||||
|
await waku.lightPush.send(td.encoders[i], {
|
||||||
|
payload: utf8ToBytes(`Message for Topic ${i + 1}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that each message was received on the corresponding topic.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(20)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
td.contentTopics.forEach((topic, index) => {
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(index, {
|
||||||
|
expectedContentTopic: topic,
|
||||||
|
expectedMessageText: `Message for Topic ${index + 1}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Subscribe to 30 topics at once and receives messages", async function () {
|
||||||
|
const topicCount = 30;
|
||||||
|
const td = generateTestData(topicCount);
|
||||||
|
|
||||||
|
// Subscribe to all 30 topics.
|
||||||
|
await subscription.subscribe(
|
||||||
|
td.decoders,
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send a unique message on each topic.
|
||||||
|
for (let i = 0; i < topicCount; i++) {
|
||||||
|
await waku.lightPush.send(td.encoders[i], {
|
||||||
|
payload: utf8ToBytes(`Message for Topic ${i + 1}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that each message was received on the corresponding topic.
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(30)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
td.contentTopics.forEach((topic, index) => {
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(index, {
|
||||||
|
expectedContentTopic: topic,
|
||||||
|
expectedMessageText: `Message for Topic ${index + 1}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Error when try to subscribe to more than 30 topics", async function () {
|
||||||
|
const topicCount = 31;
|
||||||
|
const td = generateTestData(topicCount);
|
||||||
|
|
||||||
|
// Attempt to subscribe to 31 topics
|
||||||
|
try {
|
||||||
|
await subscription.subscribe(
|
||||||
|
td.decoders,
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"Subscribe to 31 topics was successful but was expected to fail with a specific error."
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof Error &&
|
||||||
|
err.message.includes("exceeds maximum content topics: 30")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Overlapping topic subscription", async function () {
|
||||||
|
// Define two sets of test data with overlapping topics.
|
||||||
|
const topicCount1 = 2;
|
||||||
|
const td1 = generateTestData(topicCount1);
|
||||||
|
const topicCount2 = 4;
|
||||||
|
const td2 = generateTestData(topicCount2);
|
||||||
|
|
||||||
|
// Subscribe to the first set of topics.
|
||||||
|
await subscription.subscribe(
|
||||||
|
td1.decoders,
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe to the second set of topics which has overlapping topics with the first set.
|
||||||
|
await subscription.subscribe(
|
||||||
|
td2.decoders,
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send messages to the first set of topics.
|
||||||
|
for (let i = 0; i < topicCount1; i++) {
|
||||||
|
const messageText = `Topic Set 1: Message Number: ${i + 1}`;
|
||||||
|
await waku.lightPush.send(td1.encoders[i], {
|
||||||
|
payload: utf8ToBytes(messageText)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send messages to the second set of topics.
|
||||||
|
for (let i = 0; i < topicCount2; i++) {
|
||||||
|
const messageText = `Topic Set 2: Message Number: ${i + 1}`;
|
||||||
|
await waku.lightPush.send(td2.encoders[i], {
|
||||||
|
payload: utf8ToBytes(messageText)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all messages were received.
|
||||||
|
// Since there are overlapping topics, there should be 6 messages in total (2 from the first set + 4 from the second set).
|
||||||
|
expect(
|
||||||
|
await serviceNodes.messageCollector.waitForMessages(6, { exact: true })
|
||||||
|
).to.eq(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Refresh subscription", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
|
||||||
|
// Resubscribe (refresh) to the same topic and send another message.
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
|
||||||
|
// Confirm both messages were received.
|
||||||
|
expect(
|
||||||
|
await serviceNodes.messageCollector.waitForMessages(2, { exact: true })
|
||||||
|
).to.eq(true);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: "M1",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedMessageText: "M2",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST_STRING.forEach((testItem) => {
|
||||||
|
it(`Subscribe to topic containing ${testItem.description} and receive message`, async function () {
|
||||||
|
const newContentTopic = testItem.value;
|
||||||
|
const newEncoder = createEncoder({ contentTopic: newContentTopic });
|
||||||
|
const newDecoder = createDecoder(newContentTopic);
|
||||||
|
|
||||||
|
await subscription.subscribe(
|
||||||
|
[newDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(newEncoder, messagePayload);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: newContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Add multiple subscription objects on single nwaku node", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
|
||||||
|
// Create a second subscription on a different topic
|
||||||
|
const subscription2 = await waku.filter.createSubscription();
|
||||||
|
const newContentTopic = "/test/2/waku-filter";
|
||||||
|
const newEncoder = createEncoder({ contentTopic: newContentTopic });
|
||||||
|
const newDecoder = createDecoder(newContentTopic);
|
||||||
|
await subscription2.subscribe(
|
||||||
|
[newDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
|
||||||
|
await waku.lightPush.send(newEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
|
||||||
|
// Check if both messages were received
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: "M1",
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(1, {
|
||||||
|
expectedContentTopic: newContentTopic,
|
||||||
|
expectedMessageText: "M2"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[true, false].map((strictCheckNodes) => runTests(strictCheckNodes));
|
216
packages/tests/tests/filter/unsubscribe.node.spec.ts
Normal file
216
packages/tests/tests/filter/unsubscribe.node.spec.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import { createDecoder, createEncoder } from "@waku/core";
|
||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
IFilterSubscription,
|
||||||
|
LightNode
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { generateTestData, ServiceNodesFleet } from "../../src/index.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
messagePayload,
|
||||||
|
messageText,
|
||||||
|
runMultipleNodes,
|
||||||
|
teardownNodesWithRedundancy,
|
||||||
|
TestContentTopic,
|
||||||
|
TestDecoder,
|
||||||
|
TestEncoder
|
||||||
|
} from "./utils.js";
|
||||||
|
|
||||||
|
const runTests = (strictCheckNodes: boolean): void => {
|
||||||
|
describe(`Waku Filter V2: Unsubscribe: Multiple Nodes: Strict Checking: ${strictCheckNodes}`, function () {
|
||||||
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
|
this.timeout(10000);
|
||||||
|
let waku: LightNode;
|
||||||
|
let serviceNodes: ServiceNodesFleet;
|
||||||
|
let subscription: IFilterSubscription;
|
||||||
|
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
[serviceNodes, waku] = await runMultipleNodes(this, [DefaultPubsubTopic]);
|
||||||
|
subscription = await waku.filter.createSubscription();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribe 1 topic - node subscribed to 1 topic", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, messagePayload);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unsubscribe from the topic and send again
|
||||||
|
await subscription.unsubscribe([TestContentTopic]);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that from 2 messages send only the 1st was received
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(1);
|
||||||
|
|
||||||
|
await serviceNodes.confirmMessageLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribe 1 topic - node subscribed to 2 topics", async function () {
|
||||||
|
// Subscribe to 2 topics and send messages
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
const newContentTopic = "/test/2/waku-filter";
|
||||||
|
const newEncoder = createEncoder({ contentTopic: newContentTopic });
|
||||||
|
const newDecoder = createDecoder(newContentTopic);
|
||||||
|
await subscription.subscribe(
|
||||||
|
[newDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
await waku.lightPush.send(newEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unsubscribe from the first topic and send again
|
||||||
|
await subscription.unsubscribe([TestContentTopic]);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M3") });
|
||||||
|
await waku.lightPush.send(newEncoder, { payload: utf8ToBytes("M4") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(3)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that from 4 messages send 3 were received
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(3);
|
||||||
|
await serviceNodes.confirmMessageLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribe 2 topics - node subscribed to 2 topics", async function () {
|
||||||
|
// Subscribe to 2 topics and send messages
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
const newContentTopic = "/test/2/waku-filter";
|
||||||
|
const newEncoder = createEncoder({ contentTopic: newContentTopic });
|
||||||
|
const newDecoder = createDecoder(newContentTopic);
|
||||||
|
await subscription.subscribe(
|
||||||
|
[newDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
await waku.lightPush.send(newEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unsubscribe from both and send again
|
||||||
|
await subscription.unsubscribe([TestContentTopic, newContentTopic]);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M3") });
|
||||||
|
await waku.lightPush.send(newEncoder, { payload: utf8ToBytes("M4") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(3)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that from 4 messages send 2 were received
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(2);
|
||||||
|
await serviceNodes.confirmMessageLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribe topics the node is not subscribed to", async function () {
|
||||||
|
// Subscribe to 1 topic and send message
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(1);
|
||||||
|
|
||||||
|
// Unsubscribe from topics that the node is not not subscribed to and send again
|
||||||
|
await subscription.unsubscribe([]);
|
||||||
|
await subscription.unsubscribe(["/test/2/waku-filter"]);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that both messages were received
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(2);
|
||||||
|
await serviceNodes.confirmMessageLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribes all - node subscribed to 1 topic", async function () {
|
||||||
|
await subscription.subscribe(
|
||||||
|
[TestDecoder],
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M1") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(1);
|
||||||
|
|
||||||
|
// Unsubscribe from all topics and send again
|
||||||
|
await subscription.unsubscribeAll();
|
||||||
|
await waku.lightPush.send(TestEncoder, { payload: utf8ToBytes("M2") });
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(2)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that from 2 messages send only the 1st was received
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(1);
|
||||||
|
await serviceNodes.confirmMessageLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Unsubscribes all - node subscribed to 10 topics", async function () {
|
||||||
|
// Subscribe to 10 topics and send message
|
||||||
|
const topicCount = 10;
|
||||||
|
const td = generateTestData(topicCount);
|
||||||
|
await subscription.subscribe(
|
||||||
|
td.decoders,
|
||||||
|
serviceNodes.messageCollector.callback
|
||||||
|
);
|
||||||
|
for (let i = 0; i < topicCount; i++) {
|
||||||
|
await waku.lightPush.send(td.encoders[i], {
|
||||||
|
payload: utf8ToBytes(`M${i + 1}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(10)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unsubscribe from all topics and send again
|
||||||
|
await subscription.unsubscribeAll();
|
||||||
|
for (let i = 0; i < topicCount; i++) {
|
||||||
|
await waku.lightPush.send(td.encoders[i], {
|
||||||
|
payload: utf8ToBytes(`M${topicCount + i + 1}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(11)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that from 20 messages send only 10 were received
|
||||||
|
expect(serviceNodes.messageCollector.count).to.eq(10);
|
||||||
|
await serviceNodes.confirmMessageLength(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[true, false].map(runTests);
|
@ -3,15 +3,22 @@ import {
|
|||||||
DefaultPubsubTopic,
|
DefaultPubsubTopic,
|
||||||
IFilterSubscription,
|
IFilterSubscription,
|
||||||
LightNode,
|
LightNode,
|
||||||
|
ProtocolCreateOptions,
|
||||||
Protocols,
|
Protocols,
|
||||||
ShardingParams
|
ShardingParams,
|
||||||
|
Waku
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { createLightNode } from "@waku/sdk";
|
import { createLightNode } from "@waku/sdk";
|
||||||
import { Logger } from "@waku/utils";
|
import { Logger } from "@waku/utils";
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
import { utf8ToBytes } from "@waku/utils/bytes";
|
||||||
import { Context } from "mocha";
|
import { Context } from "mocha";
|
||||||
|
import pRetry from "p-retry";
|
||||||
|
|
||||||
import { makeLogFileName, NOISE_KEY_1, ServiceNode } from "../../src/index.js";
|
import {
|
||||||
|
NOISE_KEY_1,
|
||||||
|
ServiceNodesFleet,
|
||||||
|
waitForConnections
|
||||||
|
} from "../../src/index.js";
|
||||||
|
|
||||||
// Constants for test configuration.
|
// Constants for test configuration.
|
||||||
export const log = new Logger("test:filter");
|
export const log = new Logger("test:filter");
|
||||||
@ -42,28 +49,31 @@ export async function validatePingError(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runNodes(
|
export async function runMultipleNodes(
|
||||||
context: Context,
|
context: Context,
|
||||||
//TODO: change this to use `ShardInfo` instead of `string[]`
|
//TODO: change this to use `ShardInfo` instead of `string[]`
|
||||||
pubsubTopics: string[],
|
pubsubTopics: string[],
|
||||||
shardInfo?: ShardingParams
|
strictChecking: boolean = false,
|
||||||
): Promise<[ServiceNode, LightNode]> {
|
shardInfo?: ShardingParams,
|
||||||
const nwaku = new ServiceNode(makeLogFileName(context));
|
numServiceNodes = 3,
|
||||||
|
withoutFilter = false
|
||||||
await nwaku.start(
|
): Promise<[ServiceNodesFleet, LightNode]> {
|
||||||
{
|
// create numServiceNodes nodes
|
||||||
filter: true,
|
const serviceNodes = await ServiceNodesFleet.createAndRun(
|
||||||
lightpush: true,
|
context,
|
||||||
relay: true,
|
pubsubTopics,
|
||||||
pubsubTopic: pubsubTopics,
|
numServiceNodes,
|
||||||
...(shardInfo && { clusterId: shardInfo.clusterId })
|
strictChecking,
|
||||||
},
|
shardInfo,
|
||||||
{ retries: 3 }
|
undefined,
|
||||||
|
withoutFilter
|
||||||
);
|
);
|
||||||
|
|
||||||
const waku_options = {
|
const waku_options: ProtocolCreateOptions = {
|
||||||
staticNoiseKey: NOISE_KEY_1,
|
staticNoiseKey: NOISE_KEY_1,
|
||||||
libp2p: { addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] } },
|
libp2p: {
|
||||||
|
addresses: { listen: ["/ip4/0.0.0.0/tcp/0/ws"] }
|
||||||
|
},
|
||||||
pubsubTopics: shardInfo ? undefined : pubsubTopics,
|
pubsubTopics: shardInfo ? undefined : pubsubTopics,
|
||||||
...((pubsubTopics.length !== 1 ||
|
...((pubsubTopics.length !== 1 ||
|
||||||
pubsubTopics[0] !== DefaultPubsubTopic) && {
|
pubsubTopics[0] !== DefaultPubsubTopic) && {
|
||||||
@ -84,18 +94,61 @@ export async function runNodes(
|
|||||||
throw new Error("Failed to initialize waku");
|
throw new Error("Failed to initialize waku");
|
||||||
}
|
}
|
||||||
|
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
for (const node of serviceNodes.nodes) {
|
||||||
await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush]);
|
await waku.dial(await node.getMultiaddrWithId());
|
||||||
await nwaku.ensureSubscriptions(pubsubTopics);
|
await waitForRemotePeer(waku, [Protocols.Filter, Protocols.LightPush]);
|
||||||
|
await node.ensureSubscriptions(pubsubTopics);
|
||||||
|
|
||||||
const wakuConnections = waku.libp2p.getConnections();
|
const wakuConnections = waku.libp2p.getConnections();
|
||||||
const nwakuPeers = await nwaku.peers();
|
const nodePeers = await node.peers();
|
||||||
|
|
||||||
if (wakuConnections.length < 1 || nwakuPeers.length < 1) {
|
if (wakuConnections.length < 1 || nodePeers.length < 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected at least 1 peer in each node. Got waku connections: ${wakuConnections.length} and nwaku: ${nwakuPeers.length}`
|
`Expected at least 1 peer in each node. Got waku connections: ${wakuConnections.length} and service nodes: ${nodePeers.length}`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [nwaku, waku];
|
await waitForConnections(numServiceNodes, waku);
|
||||||
|
|
||||||
|
return [serviceNodes, waku];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function teardownNodesWithRedundancy(
|
||||||
|
serviceNodes: ServiceNodesFleet,
|
||||||
|
wakuNodes: Waku | Waku[]
|
||||||
|
): Promise<void> {
|
||||||
|
const wNodes = Array.isArray(wakuNodes) ? wakuNodes : [wakuNodes];
|
||||||
|
|
||||||
|
const stopNwakuNodes = serviceNodes.nodes.map(async (node) => {
|
||||||
|
await pRetry(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
await node.stop();
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Service Node failed to stop:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ retries: 3 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopWakuNodes = wNodes.map(async (waku) => {
|
||||||
|
if (waku) {
|
||||||
|
await pRetry(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
await waku.stop();
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Waku failed to stop:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ retries: 3 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([...stopNwakuNodes, ...stopWakuNodes]);
|
||||||
}
|
}
|
||||||
|
268
packages/tests/tests/light-push/index.node.spec.ts
Normal file
268
packages/tests/tests/light-push/index.node.spec.ts
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { createEncoder } from "@waku/core";
|
||||||
|
import {
|
||||||
|
DefaultPubsubTopic,
|
||||||
|
IRateLimitProof,
|
||||||
|
LightNode,
|
||||||
|
SendError
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateRandomUint8Array,
|
||||||
|
ServiceNodesFleet,
|
||||||
|
TEST_STRING
|
||||||
|
} from "../../src";
|
||||||
|
import {
|
||||||
|
runMultipleNodes,
|
||||||
|
teardownNodesWithRedundancy
|
||||||
|
} from "../filter/utils.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
messagePayload,
|
||||||
|
messageText,
|
||||||
|
TestContentTopic,
|
||||||
|
TestEncoder
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
const runTests = (strictNodeCheck: boolean): void => {
|
||||||
|
const numServiceNodes = 3;
|
||||||
|
describe(`Waku Light Push: Multiple Nodes: Strict Check: ${strictNodeCheck}`, function () {
|
||||||
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
|
this.timeout(15000);
|
||||||
|
let waku: LightNode;
|
||||||
|
let serviceNodes: ServiceNodesFleet;
|
||||||
|
|
||||||
|
this.beforeEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
[serviceNodes, waku] = await runMultipleNodes(
|
||||||
|
this,
|
||||||
|
[DefaultPubsubTopic],
|
||||||
|
strictNodeCheck,
|
||||||
|
undefined,
|
||||||
|
numServiceNodes,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.afterEach(async function () {
|
||||||
|
this.timeout(15000);
|
||||||
|
await teardownNodesWithRedundancy(serviceNodes, waku);
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST_STRING.forEach((testItem) => {
|
||||||
|
it(`Push message with ${testItem.description} payload`, async function () {
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(testItem.value)
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: testItem.value,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Push 30 different messages", async function () {
|
||||||
|
const generateMessageText = (index: number): string => `M${index}`;
|
||||||
|
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(generateMessageText(i))
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(30)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(i, {
|
||||||
|
expectedMessageText: generateMessageText(i),
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Throws when trying to push message with empty payload", async function () {
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: new Uint8Array()
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pushResponse.recipients.length).to.eq(0);
|
||||||
|
console.log("validated 1");
|
||||||
|
expect(pushResponse.errors).to.include(SendError.EMPTY_PAYLOAD);
|
||||||
|
console.log("validated 2");
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
console.log("validated 3");
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST_STRING.forEach((testItem) => {
|
||||||
|
it(`Push message with content topic containing ${testItem.description}`, async function () {
|
||||||
|
const customEncoder = createEncoder({
|
||||||
|
contentTopic: testItem.value
|
||||||
|
});
|
||||||
|
const pushResponse = await waku.lightPush.send(
|
||||||
|
customEncoder,
|
||||||
|
messagePayload
|
||||||
|
);
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: testItem.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fails to push message with empty content topic", async function () {
|
||||||
|
try {
|
||||||
|
createEncoder({ contentTopic: "" });
|
||||||
|
expect.fail("Expected an error but didn't get one");
|
||||||
|
} catch (error) {
|
||||||
|
expect((error as Error).message).to.equal(
|
||||||
|
"Content topic must be specified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Push message with meta", async function () {
|
||||||
|
const customTestEncoder = createEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
metaSetter: () => new Uint8Array(10)
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushResponse = await waku.lightPush.send(
|
||||||
|
customTestEncoder,
|
||||||
|
messagePayload
|
||||||
|
);
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fails to push message with large meta", async function () {
|
||||||
|
const customTestEncoder = createEncoder({
|
||||||
|
contentTopic: TestContentTopic,
|
||||||
|
metaSetter: () => new Uint8Array(105024) // see the note below ***
|
||||||
|
});
|
||||||
|
|
||||||
|
// *** note: this test used 10 ** 6 when `nwaku` node had MaxWakuMessageSize == 1MiB ( 1*2^20 .)
|
||||||
|
// `nwaku` establishes the max lightpush msg size as `const MaxRpcSize* = MaxWakuMessageSize + 64 * 1024`
|
||||||
|
// see: https://github.com/waku-org/nwaku/blob/07beea02095035f4f4c234ec2dec1f365e6955b8/waku/waku_lightpush/rpc_codec.nim#L15
|
||||||
|
// In the PR https://github.com/waku-org/nwaku/pull/2298 we reduced the MaxWakuMessageSize
|
||||||
|
// from 1MiB to 150KiB. Therefore, the 105024 number comes from substracting ( 1*2^20 - 150*2^10 )
|
||||||
|
// to the original 10^6 that this test had when MaxWakuMessageSize == 1*2^20
|
||||||
|
|
||||||
|
const pushResponse = await waku.lightPush.send(
|
||||||
|
customTestEncoder,
|
||||||
|
messagePayload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serviceNodes.type == "go-waku") {
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
expect(pushResponse.recipients.length).to.eq(0);
|
||||||
|
expect(pushResponse.errors).to.include(SendError.REMOTE_PEER_REJECTED);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Push message with rate limit", async function () {
|
||||||
|
const rateLimitProof: IRateLimitProof = {
|
||||||
|
proof: utf8ToBytes("proofData"),
|
||||||
|
merkleRoot: utf8ToBytes("merkleRootData"),
|
||||||
|
epoch: utf8ToBytes("epochData"),
|
||||||
|
shareX: utf8ToBytes("shareXData"),
|
||||||
|
shareY: utf8ToBytes("shareYData"),
|
||||||
|
nullifier: utf8ToBytes("nullifierData"),
|
||||||
|
rlnIdentifier: utf8ToBytes("rlnIdentifierData")
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
rateLimitProof: rateLimitProof
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
Date.now() - 3600000 * 24 * 356,
|
||||||
|
Date.now() - 3600000,
|
||||||
|
Date.now() + 3600000
|
||||||
|
].forEach((testItem) => {
|
||||||
|
it(`Push message with custom timestamp: ${testItem}`, async function () {
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: utf8ToBytes(messageText),
|
||||||
|
timestamp: new Date(testItem)
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.eq(numServiceNodes);
|
||||||
|
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
serviceNodes.messageCollector.verifyReceivedMessage(0, {
|
||||||
|
expectedMessageText: messageText,
|
||||||
|
expectedTimestamp: testItem,
|
||||||
|
expectedContentTopic: TestContentTopic
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Push message equal or less that 1MB", async function () {
|
||||||
|
const bigPayload = generateRandomUint8Array(65536);
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: bigPayload
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.greaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Fails to push message bigger that 1MB", async function () {
|
||||||
|
const MB = 1024 ** 2;
|
||||||
|
|
||||||
|
const pushResponse = await waku.lightPush.send(TestEncoder, {
|
||||||
|
payload: generateRandomUint8Array(MB + 65536)
|
||||||
|
});
|
||||||
|
expect(pushResponse.recipients.length).to.eq(0);
|
||||||
|
expect(pushResponse.errors).to.include(SendError.SIZE_TOO_BIG);
|
||||||
|
expect(await serviceNodes.messageCollector.waitForMessages(1)).to.eq(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
[true].map(runTests);
|
@ -5,7 +5,7 @@ import {
|
|||||||
LightNode,
|
LightNode,
|
||||||
SendError
|
SendError
|
||||||
} from "@waku/interfaces";
|
} from "@waku/interfaces";
|
||||||
import { utf8ToBytes } from "@waku/utils/bytes";
|
import { utf8ToBytes } from "@waku/sdk";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -23,7 +23,7 @@ import {
|
|||||||
TestEncoder
|
TestEncoder
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
|
|
||||||
describe("Waku Light Push", function () {
|
describe("Waku Light Push: Single Node", function () {
|
||||||
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
// Set the timeout for all tests in this suite. Can be overwritten at test level
|
||||||
this.timeout(15000);
|
this.timeout(15000);
|
||||||
let waku: LightNode;
|
let waku: LightNode;
|
||||||
|
@ -26,6 +26,7 @@ export async function runNodes(
|
|||||||
await nwaku.start(
|
await nwaku.start(
|
||||||
{
|
{
|
||||||
lightpush: true,
|
lightpush: true,
|
||||||
|
filter: true,
|
||||||
relay: true,
|
relay: true,
|
||||||
pubsubTopic: pubsubTopics,
|
pubsubTopic: pubsubTopics,
|
||||||
...(shardInfo && { clusterId: shardInfo.clusterId })
|
...(shardInfo && { clusterId: shardInfo.clusterId })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user