feat!: local peer discovery improvements (#2557)

* update local peer discovery, make it configurable for cache

* move to separate file

* up tests, remove local storage from tests

* pass local peer cache options

* add e2e tests

* add aditional e2e tests for local cache

* rename local-peer-cache into peer-cache

* update tests, ci

* prevent filterign ws addresses
This commit is contained in:
Sasha 2025-08-15 00:14:32 +02:00 committed by GitHub
parent 95da57a870
commit eab8ce81b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 539 additions and 292 deletions

View File

@ -33,9 +33,9 @@ module.exports = [
import: "{ wakuPeerExchangeDiscovery }", import: "{ wakuPeerExchangeDiscovery }",
}, },
{ {
name: "Local Peer Cache Discovery", name: "Peer Cache Discovery",
path: "packages/discovery/bundle/index.js", path: "packages/discovery/bundle/index.js",
import: "{ wakuLocalPeerCacheDiscovery }", import: "{ wakuPeerCacheDiscovery }",
}, },
{ {
name: "Privacy preserving protocols", name: "Privacy preserving protocols",

View File

@ -508,7 +508,7 @@ describe("ConnectionLimiter", () => {
pxPeer.addresses = [ pxPeer.addresses = [
{ multiaddr: multiaddr("/dns4/px/tcp/443/wss"), isCertified: false } { multiaddr: multiaddr("/dns4/px/tcp/443/wss"), isCertified: false }
]; ];
const localPeer = createMockPeer("l", [Tags.LOCAL]); const localPeer = createMockPeer("l", [Tags.PEER_CACHE]);
localPeer.addresses = [ localPeer.addresses = [
{ multiaddr: multiaddr("/dns4/l/tcp/443/wss"), isCertified: false } { multiaddr: multiaddr("/dns4/l/tcp/443/wss"), isCertified: false }
]; ];

View File

@ -231,7 +231,7 @@ export class ConnectionLimiter implements IConnectionLimiter {
* Returns a list of peers ordered by priority: * Returns a list of peers ordered by priority:
* - bootstrap peers * - bootstrap peers
* - peers from peer exchange * - peers from peer exchange
* - peers from local store (last because we are not sure that locally stored information is up to date) * - peers from peer cache (last because we are not sure that locally stored information is up to date)
*/ */
private async getPrioritizedPeers(): Promise<Peer[]> { private async getPrioritizedPeers(): Promise<Peer[]> {
const allPeers = await this.libp2p.peerStore.all(); const allPeers = await this.libp2p.peerStore.all();
@ -260,7 +260,7 @@ export class ConnectionLimiter implements IConnectionLimiter {
); );
const localStorePeers = notConnectedPeers.filter((p) => const localStorePeers = notConnectedPeers.filter((p) =>
p.tags.has(Tags.LOCAL) p.tags.has(Tags.PEER_CACHE)
); );
return [...bootstrapPeers, ...peerExchangePeers, ...localStorePeers]; return [...bootstrapPeers, ...peerExchangePeers, ...localStorePeers];

View File

@ -9,6 +9,6 @@ export {
} from "./peer-exchange/index.js"; } from "./peer-exchange/index.js";
export { export {
LocalPeerCacheDiscovery, PeerCacheDiscovery,
wakuLocalPeerCacheDiscovery wakuPeerCacheDiscovery
} from "./local-peer-cache/index.js"; } from "./peer-cache/index.js";

View File

@ -1,163 +0,0 @@
import { TypedEventEmitter } from "@libp2p/interface";
import {
IdentifyResult,
PeerDiscovery,
PeerDiscoveryEvents,
PeerInfo,
Startable
} from "@libp2p/interface";
import { peerIdFromString } from "@libp2p/peer-id";
import { multiaddr } from "@multiformats/multiaddr";
import {
type Libp2pComponents,
type LocalStoragePeerInfo,
Tags
} from "@waku/interfaces";
import { getWsMultiaddrFromMultiaddrs, Logger } from "@waku/utils";
const log = new Logger("local-cache-discovery");
type LocalPeerCacheDiscoveryOptions = {
tagName?: string;
tagValue?: number;
tagTTL?: number;
};
const DEFAULT_LOCAL_TAG_NAME = Tags.LOCAL;
const DEFAULT_LOCAL_TAG_VALUE = 50;
const DEFAULT_LOCAL_TAG_TTL = 100_000_000;
export class LocalPeerCacheDiscovery
extends TypedEventEmitter<PeerDiscoveryEvents>
implements PeerDiscovery, Startable
{
private isStarted: boolean;
private peers: LocalStoragePeerInfo[] = [];
public constructor(
private readonly components: Libp2pComponents,
private readonly options?: LocalPeerCacheDiscoveryOptions
) {
super();
this.isStarted = false;
this.peers = this.getPeersFromLocalStorage();
}
public get [Symbol.toStringTag](): string {
return "@waku/local-peer-cache-discovery";
}
public async start(): Promise<void> {
if (this.isStarted) return;
log.info("Starting Local Storage Discovery");
this.components.events.addEventListener(
"peer:identify",
this.handleNewPeers
);
for (const { id: idStr, address } of this.peers) {
const peerId = peerIdFromString(idStr);
if (await this.components.peerStore.has(peerId)) continue;
await this.components.peerStore.save(peerId, {
multiaddrs: [multiaddr(address)],
tags: {
[this.options?.tagName ?? DEFAULT_LOCAL_TAG_NAME]: {
value: this.options?.tagValue ?? DEFAULT_LOCAL_TAG_VALUE,
ttl: this.options?.tagTTL ?? DEFAULT_LOCAL_TAG_TTL
}
}
});
this.dispatchEvent(
new CustomEvent<PeerInfo>("peer", {
detail: {
id: peerId,
multiaddrs: [multiaddr(address)]
}
})
);
}
log.info(`Discovered ${this.peers.length} peers`);
this.isStarted = true;
}
public stop(): void | Promise<void> {
if (!this.isStarted) return;
log.info("Stopping Local Storage Discovery");
this.components.events.removeEventListener(
"peer:identify",
this.handleNewPeers
);
this.isStarted = false;
this.savePeersToLocalStorage();
}
public handleNewPeers = (event: CustomEvent<IdentifyResult>): void => {
const { peerId, listenAddrs } = event.detail;
const websocketMultiaddr = getWsMultiaddrFromMultiaddrs(listenAddrs);
const localStoragePeers = this.getPeersFromLocalStorage();
const existingPeerIndex = localStoragePeers.findIndex(
(_peer) => _peer.id === peerId.toString()
);
if (existingPeerIndex >= 0) {
localStoragePeers[existingPeerIndex].address =
websocketMultiaddr.toString();
} else {
localStoragePeers.push({
id: peerId.toString(),
address: websocketMultiaddr.toString()
});
}
this.peers = localStoragePeers;
this.savePeersToLocalStorage();
};
private getPeersFromLocalStorage(): LocalStoragePeerInfo[] {
try {
const storedPeersData = localStorage.getItem("waku:peers");
if (!storedPeersData) return [];
const peers = JSON.parse(storedPeersData);
return peers.filter(isValidStoredPeer);
} catch (error) {
log.error("Error parsing peers from local storage:", error);
return [];
}
}
private savePeersToLocalStorage(): void {
try {
localStorage.setItem("waku:peers", JSON.stringify(this.peers));
} catch (error) {
log.error("Error saving peers to local storage:", error);
}
}
}
function isValidStoredPeer(peer: any): peer is LocalStoragePeerInfo {
return (
peer &&
typeof peer === "object" &&
typeof peer.id === "string" &&
typeof peer.address === "string"
);
}
export function wakuLocalPeerCacheDiscovery(): (
components: Libp2pComponents,
options?: LocalPeerCacheDiscoveryOptions
) => LocalPeerCacheDiscovery {
return (
components: Libp2pComponents,
options?: LocalPeerCacheDiscoveryOptions
) => new LocalPeerCacheDiscovery(components, options);
}

View File

@ -0,0 +1,4 @@
import { Tags } from "@waku/interfaces";
export const DEFAULT_PEER_CACHE_TAG_NAME = Tags.PEER_CACHE;
export const DEFAULT_PEER_CACHE_TAG_VALUE = 50;

View File

@ -0,0 +1 @@
export { wakuPeerCacheDiscovery, PeerCacheDiscovery } from "./peer_cache.js";

View File

@ -6,70 +6,68 @@ import { prefixLogger } from "@libp2p/logger";
import { peerIdFromPrivateKey, peerIdFromString } from "@libp2p/peer-id"; import { peerIdFromPrivateKey, peerIdFromString } from "@libp2p/peer-id";
import { persistentPeerStore } from "@libp2p/peer-store"; import { persistentPeerStore } from "@libp2p/peer-store";
import { multiaddr } from "@multiformats/multiaddr"; import { multiaddr } from "@multiformats/multiaddr";
import { Libp2pComponents } from "@waku/interfaces"; import { Libp2pComponents, PartialPeerInfo, PeerCache } from "@waku/interfaces";
import { LocalStoragePeerInfo } from "@waku/interfaces";
import chai, { expect } from "chai"; import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised"; import chaiAsPromised from "chai-as-promised";
import { MemoryDatastore } from "datastore-core/memory"; import { MemoryDatastore } from "datastore-core/memory";
import sinon from "sinon"; import sinon from "sinon";
import { LocalPeerCacheDiscovery } from "./index.js"; import { PeerCacheDiscovery } from "./index.js";
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
if (typeof window === "undefined") { const mockPeers: PartialPeerInfo[] = [
try {
global.localStorage = {
store: {} as Record<string, string>,
getItem(key: string) {
return this.store[key] || null;
},
setItem(key: string, value: string) {
this.store[key] = value;
},
removeItem(key: string) {
delete this.store[key];
},
clear() {
this.store = {};
}
} as any;
} catch (error) {
console.error("Failed to load localStorage polyfill:", error);
}
}
const mockPeers = [
{ {
id: "16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD", id: "16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD",
address: multiaddrs: [
"/ip4/127.0.0.1/tcp/8000/ws/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD" "/ip4/127.0.0.1/tcp/8000/wss/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrD"
]
}, },
{ {
id: "16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrE", id: "16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrE",
address: multiaddrs: [
"/ip4/127.0.0.1/tcp/8001/ws/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrE" "/ip4/127.0.0.1/tcp/8001/wss/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr24iDQpSN5Qa992BCjjwgrE"
]
} }
]; ];
async function setPeersInLocalStorage( class MockPeerCache implements PeerCache {
peers: LocalStoragePeerInfo[] public data: PartialPeerInfo[] = [];
): Promise<void> { public throwOnGet = false;
localStorage.setItem("waku:peers", JSON.stringify(peers)); public get(): PartialPeerInfo[] {
if (this.throwOnGet) {
throw new Error("cache get error");
}
return this.data;
}
public set(value: PartialPeerInfo[]): void {
this.data = value;
}
public remove(): void {
this.data = [];
}
} }
describe("Local Storage Discovery", function () { async function setPeersInCache(
cache: MockPeerCache,
peers: PartialPeerInfo[]
): Promise<void> {
cache.set(peers);
}
describe("Peer Cache Discovery", function () {
this.timeout(25_000); this.timeout(25_000);
let components: Libp2pComponents; let components: Libp2pComponents;
let mockCache: MockPeerCache;
beforeEach(async function () { beforeEach(async function () {
localStorage.clear(); mockCache = new MockPeerCache();
components = { components = {
peerStore: persistentPeerStore({ peerStore: persistentPeerStore({
events: new TypedEventEmitter(), events: new TypedEventEmitter(),
peerId: await generateKeyPair("secp256k1").then(peerIdFromPrivateKey), peerId: await generateKeyPair("secp256k1").then(peerIdFromPrivateKey),
datastore: new MemoryDatastore(), datastore: new MemoryDatastore(),
logger: prefixLogger("local_discovery.spec.ts") logger: prefixLogger("peer_cache_discovery.spec.ts")
}), }),
events: new TypedEventEmitter() events: new TypedEventEmitter()
} as unknown as Libp2pComponents; } as unknown as Libp2pComponents;
@ -77,23 +75,24 @@ describe("Local Storage Discovery", function () {
describe("Compliance Tests", function () { describe("Compliance Tests", function () {
beforeEach(async function () { beforeEach(async function () {
await setPeersInLocalStorage([mockPeers[0]]); mockCache = new MockPeerCache();
await setPeersInCache(mockCache, [mockPeers[0]]);
}); });
tests({ tests({
async setup() { async setup() {
return new LocalPeerCacheDiscovery(components); return new PeerCacheDiscovery(components, { cache: mockCache });
}, },
async teardown() {} async teardown() {}
}); });
}); });
describe("Unit Tests", function () { describe("Unit Tests", function () {
let discovery: LocalPeerCacheDiscovery; let discovery: PeerCacheDiscovery;
beforeEach(async function () { beforeEach(async function () {
discovery = new LocalPeerCacheDiscovery(components); discovery = new PeerCacheDiscovery(components, { cache: mockCache });
await setPeersInLocalStorage(mockPeers); await setPeersInCache(mockCache, mockPeers);
}); });
it("should load peers from local storage and dispatch events", async () => { it("should load peers from local storage and dispatch events", async () => {
@ -103,43 +102,46 @@ describe("Local Storage Discovery", function () {
expect(dispatchEventSpy.calledWith(sinon.match.has("type", "peer"))).to.be expect(dispatchEventSpy.calledWith(sinon.match.has("type", "peer"))).to.be
.true; .true;
const dispatchedIds = dispatchEventSpy
.getCalls()
.map((c) => (c.args[0] as CustomEvent<any>).detail?.id?.toString?.())
.filter(Boolean);
mockPeers.forEach((mockPeer) => { mockPeers.forEach((mockPeer) => {
expect( expect(dispatchedIds).to.include(mockPeer.id);
dispatchEventSpy.calledWith(
sinon.match.hasNested("detail.id", mockPeer.id)
)
).to.be.true;
}); });
}); });
it("should update peers in local storage on 'peer:identify' event", async () => { it("should update peers in cache on 'peer:identify' event", async () => {
const newPeerIdentifyEvent = { await discovery.start();
detail: {
peerId: peerIdFromString(mockPeers[1].id.toString()), const newPeerIdentifyEvent = new CustomEvent<IdentifyResult>(
listenAddrs: [multiaddr(mockPeers[1].address)] "peer:identify",
{
detail: {
peerId: peerIdFromString(mockPeers[1].id.toString()),
listenAddrs: [multiaddr(mockPeers[1].multiaddrs[0])]
} as IdentifyResult
} }
} as CustomEvent<IdentifyResult>;
// Directly invoke handleNewPeers to simulate receiving an 'identify' event
discovery.handleNewPeers(newPeerIdentifyEvent);
const updatedPeers = JSON.parse(
localStorage.getItem("waku:peers") || "[]"
); );
expect(updatedPeers).to.deep.include({
id: newPeerIdentifyEvent.detail.peerId.toString(), components.events.dispatchEvent(newPeerIdentifyEvent);
address: newPeerIdentifyEvent.detail.listenAddrs[0].toString()
expect(mockCache.get()).to.deep.include({
id: mockPeers[1].id,
multiaddrs: [mockPeers[1].multiaddrs[0]]
}); });
}); });
it("should handle corrupted local storage data gracefully", async () => { it("should handle cache.get errors gracefully", async () => {
localStorage.setItem("waku:peers", "not-a-valid-json"); mockCache.throwOnGet = true;
try { try {
await discovery.start(); await discovery.start();
} catch (error) { } catch (error) {
expect.fail( expect.fail(
"start() should not have thrown an error for corrupted local storage data" "start() should not have thrown an error when cache.get throws"
); );
} }
}); });

View File

@ -0,0 +1,152 @@
import { TypedEventEmitter } from "@libp2p/interface";
import {
IdentifyResult,
PeerDiscovery,
PeerDiscoveryEvents,
PeerInfo,
Startable
} from "@libp2p/interface";
import { peerIdFromString } from "@libp2p/peer-id";
import { multiaddr } from "@multiformats/multiaddr";
import type {
Libp2pComponents,
PartialPeerInfo,
PeerCache,
PeerCacheDiscoveryOptions
} from "@waku/interfaces";
import { Logger } from "@waku/utils";
import {
DEFAULT_PEER_CACHE_TAG_NAME,
DEFAULT_PEER_CACHE_TAG_VALUE
} from "./constants.js";
import { defaultCache } from "./utils.js";
const log = new Logger("peer-cache");
export class PeerCacheDiscovery
extends TypedEventEmitter<PeerDiscoveryEvents>
implements PeerDiscovery, Startable
{
private isStarted: boolean = false;
private readonly cache: PeerCache;
public constructor(
private readonly components: Libp2pComponents,
options?: Partial<PeerCacheDiscoveryOptions>
) {
super();
this.cache = options?.cache ?? defaultCache();
}
public get [Symbol.toStringTag](): string {
return `@waku/${DEFAULT_PEER_CACHE_TAG_NAME}`;
}
public async start(): Promise<void> {
if (this.isStarted) {
return;
}
log.info("Starting Peer Cache Discovery");
this.components.events.addEventListener(
"peer:identify",
this.handleDiscoveredPeer
);
await this.discoverPeers();
this.isStarted = true;
}
public stop(): void | Promise<void> {
if (!this.isStarted) {
return;
}
log.info("Stopping Peer Cache Discovery");
this.components.events.removeEventListener(
"peer:identify",
this.handleDiscoveredPeer
);
this.isStarted = false;
}
private handleDiscoveredPeer = (event: CustomEvent<IdentifyResult>): void => {
const { peerId, listenAddrs } = event.detail;
const multiaddrs = listenAddrs.map((addr) => addr.toString());
const peerIdStr = peerId.toString();
const knownPeers = this.readPeerInfoFromCache();
const peerIndex = knownPeers.findIndex((p) => p.id === peerIdStr);
if (peerIndex !== -1) {
knownPeers[peerIndex].multiaddrs = multiaddrs;
} else {
knownPeers.push({
id: peerIdStr,
multiaddrs
});
}
this.writePeerInfoToCache(knownPeers);
};
private async discoverPeers(): Promise<void> {
const knownPeers = this.readPeerInfoFromCache();
for (const peer of knownPeers) {
const peerId = peerIdFromString(peer.id);
const multiaddrs = peer.multiaddrs.map((addr) => multiaddr(addr));
if (await this.components.peerStore.has(peerId)) {
continue;
}
await this.components.peerStore.save(peerId, {
multiaddrs,
tags: {
[DEFAULT_PEER_CACHE_TAG_NAME]: {
value: DEFAULT_PEER_CACHE_TAG_VALUE
}
}
});
this.dispatchEvent(
new CustomEvent<PeerInfo>("peer", {
detail: {
id: peerId,
multiaddrs
}
})
);
}
}
private readPeerInfoFromCache(): PartialPeerInfo[] {
try {
return this.cache.get();
} catch (error) {
log.error("Error parsing peers from cache:", error);
return [];
}
}
private writePeerInfoToCache(peers: PartialPeerInfo[]): void {
try {
this.cache.set(peers);
} catch (error) {
log.error("Error saving peers to cache:", error);
}
}
}
export function wakuPeerCacheDiscovery(
options: Partial<PeerCacheDiscoveryOptions> = {}
): (components: Libp2pComponents) => PeerCacheDiscovery {
return (components: Libp2pComponents) =>
new PeerCacheDiscovery(components, options);
}

View File

@ -0,0 +1,73 @@
import type { PartialPeerInfo, PeerCache } from "@waku/interfaces";
const isValidStoredPeer = (peer: unknown): boolean => {
return (
!!peer &&
typeof peer === "object" &&
"id" in peer &&
typeof peer.id === "string" &&
"multiaddrs" in peer &&
Array.isArray(peer.multiaddrs)
);
};
/**
* A noop cache that will be used in environments where localStorage is not available.
*/
class NoopCache implements PeerCache {
public get(): PartialPeerInfo[] {
return [];
}
public set(_value: PartialPeerInfo[]): void {
return;
}
public remove(): void {
return;
}
}
/**
* A cache that uses localStorage to store peer information.
*/
class LocalStorageCache implements PeerCache {
public get(): PartialPeerInfo[] {
try {
const cachedPeers = localStorage.getItem("waku:peers");
const peers = cachedPeers ? JSON.parse(cachedPeers) : [];
return peers.filter(isValidStoredPeer);
} catch (e) {
return [];
}
}
public set(_value: PartialPeerInfo[]): void {
try {
localStorage.setItem("waku:peers", JSON.stringify(_value));
} catch (e) {
// ignore
}
}
public remove(): void {
try {
localStorage.removeItem("waku:peers");
} catch (e) {
// ignore
}
}
}
export const defaultCache = (): PeerCache => {
try {
if (typeof localStorage !== "undefined") {
return new LocalStorageCache();
}
} catch (_e) {
// ignore
}
return new NoopCache();
};

View File

@ -7,7 +7,7 @@ import { ShardId } from "./sharding.js";
export enum Tags { export enum Tags {
BOOTSTRAP = "bootstrap", BOOTSTRAP = "bootstrap",
PEER_EXCHANGE = "peer-exchange", PEER_EXCHANGE = "peer-exchange",
LOCAL = "local-peer-cache" PEER_CACHE = "peer-cache"
} }
// Connection tag // Connection tag

View File

@ -0,0 +1,52 @@
/**
* Options for the discovery.
*/
export type DiscoveryOptions = {
peerExchange: boolean;
dns: boolean;
peerCache: boolean;
};
/**
* Partial peer information used to store in the cache.
*/
export type PartialPeerInfo = {
id: string;
multiaddrs: string[];
};
/**
* A cache interface for persisting peer information.
*/
export type PeerCache = {
/**
* Get the peer information from the cache.
*
* @returns The peer information from the cache or empty array if no peer information is found.
*/
get: () => PartialPeerInfo[];
/**
* Set the peer information in the cache.
*
* @param value The peer information to set in the cache.
*/
set: (value: PartialPeerInfo[]) => void;
/**
* Remove the peer information from the cache.
*/
remove: () => void;
};
/**
* Options for the peer cache discovery.
*/
export type PeerCacheDiscoveryOptions = {
/**
* The cache to use for getting and storing cached peer information.
*
* @default LocalStorage
*/
cache: PeerCache;
};

View File

@ -15,6 +15,6 @@ export * from "./libp2p.js";
export * from "./dns_discovery.js"; export * from "./dns_discovery.js";
export * from "./metadata.js"; export * from "./metadata.js";
export * from "./constants.js"; export * from "./constants.js";
export * from "./local_storage.js";
export * from "./sharding.js"; export * from "./sharding.js";
export * from "./health_status.js"; export * from "./health_status.js";
export * from "./discovery.js";

View File

@ -1,4 +0,0 @@
export type LocalStoragePeerInfo = {
id: string;
address: string;
};

View File

@ -1,6 +1,7 @@
import type { PeerId } from "@libp2p/interface"; import type { PeerId } from "@libp2p/interface";
import type { ConnectionManagerOptions } from "./connection_manager.js"; import type { ConnectionManagerOptions } from "./connection_manager.js";
import type { DiscoveryOptions, PeerCache } from "./discovery.js";
import type { FilterProtocolOptions } from "./filter.js"; import type { FilterProtocolOptions } from "./filter.js";
import type { CreateLibp2pOptions } from "./libp2p.js"; import type { CreateLibp2pOptions } from "./libp2p.js";
import type { LightPushProtocolOptions } from "./light_push.js"; import type { LightPushProtocolOptions } from "./light_push.js";
@ -83,13 +84,17 @@ export type CreateNodeOptions = {
/** /**
* Enable or disable specific discovery methods. * Enable or disable specific discovery methods.
* *
* @default { peerExchange: true, dns: true, localPeerCache: true } * @default { peerExchange: true, dns: true, peerCache: true }
*/ */
discovery?: { discovery?: Partial<DiscoveryOptions>;
peerExchange: boolean;
dns: boolean; /**
localPeerCache: boolean; * Peer cache to use for storing and retrieving peer information.
}; * If present, enables peer cache discovery.
*
* @default browser's localStorage
*/
peerCache?: PeerCache;
/** /**
* List of peers to use to bootstrap the node. Ignored if defaultBootstrap is set to true. * List of peers to use to bootstrap the node. Ignored if defaultBootstrap is set to true.

View File

@ -12,16 +12,16 @@ describe("Default Peer Discoveries", () => {
const discoveries = getPeerDiscoveries({ const discoveries = getPeerDiscoveries({
dns: true, dns: true,
peerExchange: true, peerExchange: true,
localPeerCache: true peerCache: true
}); });
expect(discoveries.length).to.equal(3); expect(discoveries.length).to.equal(3);
}); });
it("should enable only peerExchange and localPeerCache when dns is disabled", () => { it("should enable only peerExchange and peerCache when dns is disabled", () => {
const discoveries = getPeerDiscoveries({ const discoveries = getPeerDiscoveries({
dns: false, dns: false,
peerExchange: true, peerExchange: true,
localPeerCache: true peerCache: true
}); });
expect(discoveries.length).to.equal(2); expect(discoveries.length).to.equal(2);
}); });
@ -30,25 +30,25 @@ describe("Default Peer Discoveries", () => {
const discoveries = getPeerDiscoveries({ const discoveries = getPeerDiscoveries({
dns: true, dns: true,
peerExchange: false, peerExchange: false,
localPeerCache: true peerCache: true
}); });
expect(discoveries.length).to.equal(2); expect(discoveries.length).to.equal(2);
}); });
it("should enable only dns and peerExchange when localPeerCache is disabled", () => { it("should enable only dns and peerExchange when peerCache is disabled", () => {
const discoveries = getPeerDiscoveries({ const discoveries = getPeerDiscoveries({
dns: true, dns: true,
peerExchange: true, peerExchange: true,
localPeerCache: false peerCache: false
}); });
expect(discoveries.length).to.equal(2); expect(discoveries.length).to.equal(2);
}); });
it("should enable only localPeerCache when dns and peerExchange are disabled", () => { it("should enable only peerCache when dns and peerExchange are disabled", () => {
const discoveries = getPeerDiscoveries({ const discoveries = getPeerDiscoveries({
dns: false, dns: false,
peerExchange: false, peerExchange: false,
localPeerCache: true peerCache: true
}); });
expect(discoveries.length).to.equal(1); expect(discoveries.length).to.equal(1);
}); });

View File

@ -2,13 +2,14 @@ import type { PeerDiscovery } from "@libp2p/interface";
import { import {
enrTree, enrTree,
wakuDnsDiscovery, wakuDnsDiscovery,
wakuLocalPeerCacheDiscovery, wakuPeerCacheDiscovery,
wakuPeerExchangeDiscovery wakuPeerExchangeDiscovery
} from "@waku/discovery"; } from "@waku/discovery";
import { CreateNodeOptions, type Libp2pComponents } from "@waku/interfaces"; import { CreateNodeOptions, type Libp2pComponents } from "@waku/interfaces";
export function getPeerDiscoveries( export function getPeerDiscoveries(
enabled?: CreateNodeOptions["discovery"] enabled?: CreateNodeOptions["discovery"],
peerCache?: CreateNodeOptions["peerCache"]
): ((components: Libp2pComponents) => PeerDiscovery)[] { ): ((components: Libp2pComponents) => PeerDiscovery)[] {
const dnsEnrTrees = [enrTree["SANDBOX"], enrTree["TEST"]]; const dnsEnrTrees = [enrTree["SANDBOX"], enrTree["TEST"]];
@ -18,8 +19,8 @@ export function getPeerDiscoveries(
discoveries.push(wakuDnsDiscovery(dnsEnrTrees)); discoveries.push(wakuDnsDiscovery(dnsEnrTrees));
} }
if (enabled?.localPeerCache) { if (enabled?.peerCache || peerCache) {
discoveries.push(wakuLocalPeerCacheDiscovery()); discoveries.push(wakuPeerCacheDiscovery({ cache: peerCache }));
} }
if (enabled?.peerExchange) { if (enabled?.peerExchange) {

View File

@ -68,12 +68,6 @@ export async function defaultLibp2p(
}) as any as Libp2p; // TODO: make libp2p include it; }) as any as Libp2p; // TODO: make libp2p include it;
} }
const DEFAULT_DISCOVERIES_ENABLED = {
dns: true,
peerExchange: true,
localPeerCache: true
};
export async function createLibp2pAndUpdateOptions( export async function createLibp2pAndUpdateOptions(
options: CreateNodeOptions options: CreateNodeOptions
): Promise<Libp2p> { ): Promise<Libp2p> {
@ -87,13 +81,20 @@ export async function createLibp2pAndUpdateOptions(
if (options?.defaultBootstrap) { if (options?.defaultBootstrap) {
peerDiscovery.push( peerDiscovery.push(
...getPeerDiscoveries({ ...getPeerDiscoveries(
...DEFAULT_DISCOVERIES_ENABLED, {
...options.discovery dns: true,
}) peerExchange: true,
peerCache: true,
...options.discovery
},
options.peerCache
)
); );
} else { } else {
peerDiscovery.push(...getPeerDiscoveries(options.discovery)); peerDiscovery.push(
...getPeerDiscoveries(options.discovery, options.peerCache)
);
} }
const bootstrapPeers = [ const bootstrapPeers = [

View File

@ -0,0 +1,144 @@
import type { LightNode, PartialPeerInfo, PeerCache } from "@waku/interfaces";
import { createLightNode } from "@waku/sdk";
import { expect } from "chai";
import Sinon, { SinonSpy } from "sinon";
import {
afterEachCustom,
beforeEachCustom,
DefaultTestClusterId,
DefaultTestNetworkConfig,
DefaultTestShardInfo,
makeLogFileName,
ServiceNode,
tearDownNodes
} from "../../src/index.js";
class MockPeerCache implements PeerCache {
public data: PartialPeerInfo[] = [];
public get(): PartialPeerInfo[] {
return this.data;
}
public set(value: PartialPeerInfo[]): void {
this.data = value;
}
public remove(): void {
this.data = [];
}
}
describe("Peer Cache Discovery", function () {
this.timeout(150_000);
let ctx: Mocha.Context;
let waku: LightNode;
let nwaku1: ServiceNode;
let nwaku2: ServiceNode;
let dialPeerSpy: SinonSpy;
beforeEachCustom(this, async () => {
ctx = this.ctx;
nwaku1 = new ServiceNode(makeLogFileName(ctx) + "1");
nwaku2 = new ServiceNode(makeLogFileName(ctx) + "2");
await nwaku1.start({
clusterId: DefaultTestClusterId,
shard: DefaultTestShardInfo.shards,
discv5Discovery: true,
peerExchange: true,
relay: true
});
await nwaku2.start({
clusterId: DefaultTestClusterId,
shard: DefaultTestShardInfo.shards,
discv5Discovery: true,
peerExchange: true,
discv5BootstrapNode: (await nwaku1.info()).enrUri,
relay: true
});
});
afterEachCustom(this, async () => {
await tearDownNodes([nwaku1, nwaku2], waku);
});
it("should discover peers from provided peer cache", async function () {
const mockCache = new MockPeerCache();
mockCache.set([
{
id: (await nwaku1.getPeerId()).toString(),
multiaddrs: [(await nwaku1.getMultiaddrWithId()).toString()]
},
{
id: (await nwaku2.getPeerId()).toString(),
multiaddrs: [(await nwaku2.getMultiaddrWithId()).toString()]
}
]);
waku = await createLightNode({
networkConfig: DefaultTestNetworkConfig,
discovery: {
peerExchange: true,
peerCache: true
},
peerCache: mockCache
});
dialPeerSpy = Sinon.spy((waku as any).libp2p, "dial");
const discoveredPeers = new Set<string>();
await new Promise<void>((resolve) => {
waku.libp2p.addEventListener("peer:identify", (evt) => {
const peerId = evt.detail.peerId;
discoveredPeers.add(peerId.toString());
if (discoveredPeers.size === 2) {
resolve();
}
});
});
expect(dialPeerSpy.callCount).to.equal(2);
expect(discoveredPeers.size).to.equal(2);
});
it("should monitor connected peers and store them into cache", async function () {
const mockCache = new MockPeerCache();
waku = await createLightNode({
networkConfig: DefaultTestNetworkConfig,
bootstrapPeers: [(await nwaku2.getMultiaddrWithId()).toString()],
discovery: {
peerExchange: true,
peerCache: true
},
peerCache: mockCache
});
const discoveredPeers = new Set<string>();
await new Promise<void>((resolve) => {
waku.libp2p.addEventListener("peer:identify", (evt) => {
const peerId = evt.detail.peerId;
discoveredPeers.add(peerId.toString());
if (discoveredPeers.size === 1) {
resolve();
}
});
});
expect(discoveredPeers.size).to.equal(1);
const cachedPeers = mockCache.get();
expect(cachedPeers.length).to.equal(1);
expect(discoveredPeers.has(cachedPeers[0].id)).to.be.true;
});
});

View File

@ -1,4 +1,3 @@
import type { Multiaddr } from "@multiformats/multiaddr";
export * from "./is_defined.js"; export * from "./is_defined.js";
export * from "./random_subset.js"; export * from "./random_subset.js";
export * from "./group_by.js"; export * from "./group_by.js";
@ -8,23 +7,3 @@ export * from "./sharding/index.js";
export * from "./push_or_init_map.js"; export * from "./push_or_init_map.js";
export * from "./relay_shard_codec.js"; export * from "./relay_shard_codec.js";
export * from "./delay.js"; export * from "./delay.js";
export function removeItemFromArray(arr: unknown[], value: unknown): unknown[] {
const index = arr.indexOf(value);
if (index > -1) {
arr.splice(index, 1);
}
return arr;
}
export function getWsMultiaddrFromMultiaddrs(
addresses: Multiaddr[]
): Multiaddr {
const wsMultiaddr = addresses.find(
(addr) => addr.toString().includes("ws") || addr.toString().includes("wss")
);
if (!wsMultiaddr) {
throw new Error("No ws multiaddr found in the given addresses");
}
return wsMultiaddr;
}