mirror of
https://github.com/status-im/js-waku.git
synced 2025-02-23 10:28:15 +00:00
feat!: implement peer exchange (#1027)
* wip -- yet to test * update: draft * wip * support passing flags manually to nwaku node * refactor peer-exchange test * switch response from uint8array to ENR * rm: unnecesary logs * implement clas * fix: for loop * init-wip: directories * setup: new package & fix circular deps * bind a response handler * wip: refactor & update test * test logs * wip code - debugging * address: comments * Update packages/core/src/lib/waku_peer_exchange/peer_discovery.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * Update packages/core/src/lib/waku_peer_exchange/peer_discovery.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * address: comments * address: comments * address: comments * address: comments * address: comments * fix: test build * refactor * fix: build * comply with API * numPeers: use number instead of bigint * fix: build * Update packages/peer-exchange/package.json Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * Update packages/peer-exchange/src/waku_peer_exchange.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * Update packages/peer-exchange/src/waku_peer_exchange.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * Update packages/peer-exchange/src/waku_peer_exchange.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * address: comments, add eslint config * Update packages/peer-exchange/.eslintrc.cjs Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * Update packages/peer-exchange/src/index.ts Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * address comments * test works with test fleet * rm: only for px test => run all tests * fix: tests * reorder packages for build, and fix imports * remove: px test doesnt work with local nodes * chore: move proto into a separate package * fix: proto dir * fix: build * fix: ci * add: index for proto * fix: ci * Update packages/proto/package.json Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com> * address comments * chore: run failing test with higher timeout * chore: run failing test with higher timeout * fix: ci Co-authored-by: fryorcraken.eth <110212804+fryorcraken@users.noreply.github.com>
This commit is contained in:
parent
56009d3b26
commit
0ca7fd790c
11
.cspell.json
11
.cspell.json
@ -4,11 +4,12 @@
|
|||||||
"language": "en",
|
"language": "en",
|
||||||
"words": [
|
"words": [
|
||||||
"abortable",
|
"abortable",
|
||||||
|
"Alives",
|
||||||
"asym",
|
"asym",
|
||||||
"backoff",
|
"backoff",
|
||||||
"backoffs",
|
"backoffs",
|
||||||
"bitjson",
|
|
||||||
"bitauth",
|
"bitauth",
|
||||||
|
"bitjson",
|
||||||
"bufbuild",
|
"bufbuild",
|
||||||
"chainsafe",
|
"chainsafe",
|
||||||
"cimg",
|
"cimg",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"commitlint",
|
"commitlint",
|
||||||
"dependabot",
|
"dependabot",
|
||||||
"dingpu",
|
"dingpu",
|
||||||
|
"discv",
|
||||||
"Dlazy",
|
"Dlazy",
|
||||||
"dnsaddr",
|
"dnsaddr",
|
||||||
"Dout",
|
"Dout",
|
||||||
@ -54,13 +56,13 @@
|
|||||||
"livechat",
|
"livechat",
|
||||||
"Merkle",
|
"Merkle",
|
||||||
"mkdir",
|
"mkdir",
|
||||||
|
"mplex",
|
||||||
"multiaddr",
|
"multiaddr",
|
||||||
"multiaddresses",
|
"multiaddresses",
|
||||||
"multiaddrs",
|
"multiaddrs",
|
||||||
"multicodec",
|
"multicodec",
|
||||||
"multicodecs",
|
"multicodecs",
|
||||||
"multiformats",
|
"multiformats",
|
||||||
"mplex",
|
|
||||||
"multihashes",
|
"multihashes",
|
||||||
"muxed",
|
"muxed",
|
||||||
"muxer",
|
"muxer",
|
||||||
@ -102,13 +104,12 @@
|
|||||||
"varint",
|
"varint",
|
||||||
"waku",
|
"waku",
|
||||||
"wakuconnect",
|
"wakuconnect",
|
||||||
"wakuv",
|
|
||||||
"wakunode",
|
"wakunode",
|
||||||
|
"wakuv",
|
||||||
"webfonts",
|
"webfonts",
|
||||||
"websockets",
|
"websockets",
|
||||||
"wifi",
|
"wifi",
|
||||||
"xsalsa20",
|
"xsalsa20"
|
||||||
"Alives"
|
|
||||||
],
|
],
|
||||||
"flagWords": [],
|
"flagWords": [],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
|
20396
package-lock.json
generated
20396
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@
|
|||||||
"packages/enr",
|
"packages/enr",
|
||||||
"packages/interfaces",
|
"packages/interfaces",
|
||||||
"packages/libp2p-utils",
|
"packages/libp2p-utils",
|
||||||
|
"packages/peer-exchange",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/dns-discovery",
|
"packages/dns-discovery",
|
||||||
"packages/message-encryption",
|
"packages/message-encryption",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { PeerProtocolsChangeData } from "@libp2p/interface-peer-store";
|
import { PeerProtocolsChangeData } from "@libp2p/interface-peer-store";
|
||||||
import type { PointToPointProtocol, Relay, Waku } from "@waku/interfaces";
|
import type { PointToPointProtocol, Relay, Waku } from "@waku/interfaces";
|
||||||
import { Protocols } from "@waku/interfaces";
|
import { Protocols } from "@waku/interfaces";
|
||||||
|
import { PeerExchangeCodec } from "@waku/peer-exchange";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import { pEvent } from "p-event";
|
import { pEvent } from "p-event";
|
||||||
|
|
||||||
@ -64,6 +65,14 @@ export async function waitForRemotePeer(
|
|||||||
promises.push(waitForConnectedPeer(waku.filter, [FilterCodec]));
|
promises.push(waitForConnectedPeer(waku.filter, [FilterCodec]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (protocols.includes(Protocols.PeerExchange)) {
|
||||||
|
if (!waku.peerExchange)
|
||||||
|
throw new Error(
|
||||||
|
"Cannot wait for Peer Exchange peer: protocol not mounted"
|
||||||
|
);
|
||||||
|
promises.push(waitForConnectedPeer(waku.peerExchange, [PeerExchangeCodec]));
|
||||||
|
}
|
||||||
|
|
||||||
if (timeoutMs) {
|
if (timeoutMs) {
|
||||||
await rejectOnTimeout(
|
await rejectOnTimeout(
|
||||||
Promise.all(promises),
|
Promise.all(promises),
|
||||||
@ -146,6 +155,9 @@ function getEnabledProtocols(waku: Waku): Protocols[] {
|
|||||||
if (waku.lightPush) {
|
if (waku.lightPush) {
|
||||||
protocols.push(Protocols.LightPush);
|
protocols.push(Protocols.LightPush);
|
||||||
}
|
}
|
||||||
|
if (waku.peerExchange) {
|
||||||
|
protocols.push(Protocols.PeerExchange);
|
||||||
|
}
|
||||||
|
|
||||||
return protocols;
|
return protocols;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,17 @@ import type { Stream } from "@libp2p/interface-connection";
|
|||||||
import type { PeerId } from "@libp2p/interface-peer-id";
|
import type { PeerId } from "@libp2p/interface-peer-id";
|
||||||
import type { PubSub } from "@libp2p/interface-pubsub";
|
import type { PubSub } from "@libp2p/interface-pubsub";
|
||||||
import type { Multiaddr } from "@multiformats/multiaddr";
|
import type { Multiaddr } from "@multiformats/multiaddr";
|
||||||
import type { Filter, LightPush, Relay, Store, Waku } from "@waku/interfaces";
|
import type {
|
||||||
|
Filter,
|
||||||
|
LightPush,
|
||||||
|
PeerExchange,
|
||||||
|
PeerExchangeComponents,
|
||||||
|
Relay,
|
||||||
|
Store,
|
||||||
|
Waku,
|
||||||
|
} from "@waku/interfaces";
|
||||||
import { Protocols } from "@waku/interfaces";
|
import { Protocols } from "@waku/interfaces";
|
||||||
|
import { PeerExchangeCodec } from "@waku/peer-exchange";
|
||||||
import debug from "debug";
|
import debug from "debug";
|
||||||
import type { Libp2p } from "libp2p";
|
import type { Libp2p } from "libp2p";
|
||||||
|
|
||||||
@ -51,6 +60,7 @@ export class WakuNode implements Waku {
|
|||||||
public store?: Store;
|
public store?: Store;
|
||||||
public filter?: Filter;
|
public filter?: Filter;
|
||||||
public lightPush?: LightPush;
|
public lightPush?: LightPush;
|
||||||
|
public peerExchange?: PeerExchange;
|
||||||
|
|
||||||
private pingKeepAliveTimers: {
|
private pingKeepAliveTimers: {
|
||||||
[peer: string]: ReturnType<typeof setInterval>;
|
[peer: string]: ReturnType<typeof setInterval>;
|
||||||
@ -64,7 +74,8 @@ export class WakuNode implements Waku {
|
|||||||
libp2p: Libp2p,
|
libp2p: Libp2p,
|
||||||
store?: (components: StoreComponents) => Store,
|
store?: (components: StoreComponents) => Store,
|
||||||
lightPush?: (components: LightPushComponents) => LightPush,
|
lightPush?: (components: LightPushComponents) => LightPush,
|
||||||
filter?: (components: FilterComponents) => Filter
|
filter?: (components: FilterComponents) => Filter,
|
||||||
|
peerExchange?: (components: PeerExchangeComponents) => PeerExchange
|
||||||
) {
|
) {
|
||||||
this.libp2p = libp2p;
|
this.libp2p = libp2p;
|
||||||
|
|
||||||
@ -81,6 +92,10 @@ export class WakuNode implements Waku {
|
|||||||
this.lightPush = lightPush(components);
|
this.lightPush = lightPush(components);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (peerExchange) {
|
||||||
|
this.peerExchange = peerExchange(components);
|
||||||
|
}
|
||||||
|
|
||||||
if (isRelay(libp2p.pubsub)) {
|
if (isRelay(libp2p.pubsub)) {
|
||||||
this.relay = libp2p.pubsub;
|
this.relay = libp2p.pubsub;
|
||||||
}
|
}
|
||||||
@ -89,7 +104,8 @@ export class WakuNode implements Waku {
|
|||||||
"Waku node created",
|
"Waku node created",
|
||||||
this.libp2p.peerId.toString(),
|
this.libp2p.peerId.toString(),
|
||||||
`relay: ${!!this.relay}, store: ${!!this.store}, light push: ${!!this
|
`relay: ${!!this.relay}, store: ${!!this.store}, light push: ${!!this
|
||||||
.lightPush}, filter: ${!!this.filter}`
|
.lightPush}, filter: ${!!this.filter}, peer exchange: ${!!this
|
||||||
|
.peerExchange} `
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pingKeepAliveTimers = {};
|
this.pingKeepAliveTimers = {};
|
||||||
@ -147,6 +163,7 @@ export class WakuNode implements Waku {
|
|||||||
this.store && _protocols.push(Protocols.Store);
|
this.store && _protocols.push(Protocols.Store);
|
||||||
this.filter && _protocols.push(Protocols.Filter);
|
this.filter && _protocols.push(Protocols.Filter);
|
||||||
this.lightPush && _protocols.push(Protocols.LightPush);
|
this.lightPush && _protocols.push(Protocols.LightPush);
|
||||||
|
this.peerExchange && _protocols.push(Protocols.PeerExchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codecs: string[] = [];
|
const codecs: string[] = [];
|
||||||
@ -163,6 +180,12 @@ export class WakuNode implements Waku {
|
|||||||
codecs.push(FilterCodec);
|
codecs.push(FilterCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_protocols.includes(Protocols.PeerExchange)) {
|
||||||
|
codecs.push(PeerExchangeCodec);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Dialing to ${peer.toString()} with protocols ${_protocols}`);
|
||||||
|
|
||||||
return this.libp2p.dialProtocol(peer, codecs);
|
return this.libp2p.dialProtocol(peer, codecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +67,6 @@
|
|||||||
"@libp2p/interface-transport": "^2.0.1",
|
"@libp2p/interface-transport": "^2.0.1",
|
||||||
"@libp2p/mplex": "^7.0.0",
|
"@libp2p/mplex": "^7.0.0",
|
||||||
"@libp2p/websockets": "^5.0.0",
|
"@libp2p/websockets": "^5.0.0",
|
||||||
"@waku/core": "*",
|
|
||||||
"@waku/interfaces": "*",
|
|
||||||
"interface-datastore": "^7.0.1"
|
"interface-datastore": "^7.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
import { DefaultUserAgent } from "@waku/core";
|
import { DefaultUserAgent } from "@waku/core";
|
||||||
import { getPredefinedBootstrapNodes } from "@waku/core/lib/predefined_bootstrap_nodes";
|
import { getPredefinedBootstrapNodes } from "@waku/core/lib/predefined_bootstrap_nodes";
|
||||||
import type { Relay, WakuFull, WakuLight, WakuPrivacy } from "@waku/interfaces";
|
import type { Relay, WakuFull, WakuLight, WakuPrivacy } from "@waku/interfaces";
|
||||||
|
import { wakuPeerExchange } from "@waku/peer-exchange";
|
||||||
import type { Libp2p } from "libp2p";
|
import type { Libp2p } from "libp2p";
|
||||||
import { createLibp2p, Libp2pOptions } from "libp2p";
|
import { createLibp2p, Libp2pOptions } from "libp2p";
|
||||||
|
|
||||||
@ -85,13 +86,15 @@ export async function createLightNode(
|
|||||||
const store = wakuStore(options);
|
const store = wakuStore(options);
|
||||||
const lightPush = wakuLightPush(options);
|
const lightPush = wakuLightPush(options);
|
||||||
const filter = wakuFilter(options);
|
const filter = wakuFilter(options);
|
||||||
|
const peerExchange = wakuPeerExchange(options);
|
||||||
|
|
||||||
return new WakuNode(
|
return new WakuNode(
|
||||||
options ?? {},
|
options ?? {},
|
||||||
libp2p,
|
libp2p,
|
||||||
store,
|
store,
|
||||||
lightPush,
|
lightPush,
|
||||||
filter
|
filter,
|
||||||
|
peerExchange
|
||||||
) as WakuLight;
|
) as WakuLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,13 +153,15 @@ export async function createFullNode(
|
|||||||
const store = wakuStore(options);
|
const store = wakuStore(options);
|
||||||
const lightPush = wakuLightPush(options);
|
const lightPush = wakuLightPush(options);
|
||||||
const filter = wakuFilter(options);
|
const filter = wakuFilter(options);
|
||||||
|
const peerExchange = wakuPeerExchange(options);
|
||||||
|
|
||||||
return new WakuNode(
|
return new WakuNode(
|
||||||
options ?? {},
|
options ?? {},
|
||||||
libp2p,
|
libp2p,
|
||||||
store,
|
store,
|
||||||
lightPush,
|
lightPush,
|
||||||
filter
|
filter,
|
||||||
|
peerExchange
|
||||||
) as WakuFull;
|
) as WakuFull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import type { GossipSub } from "@chainsafe/libp2p-gossipsub";
|
import type { GossipSub } from "@chainsafe/libp2p-gossipsub";
|
||||||
import type { Stream } from "@libp2p/interface-connection";
|
import type { Stream } from "@libp2p/interface-connection";
|
||||||
|
import type { ConnectionManager } from "@libp2p/interface-connection-manager";
|
||||||
import type { PeerId } from "@libp2p/interface-peer-id";
|
import type { PeerId } from "@libp2p/interface-peer-id";
|
||||||
import type { Peer } from "@libp2p/interface-peer-store";
|
import type { Peer } from "@libp2p/interface-peer-store";
|
||||||
import type { PeerStore } from "@libp2p/interface-peer-store";
|
import type { PeerStore } from "@libp2p/interface-peer-store";
|
||||||
|
import type { Registrar } from "@libp2p/interface-registrar";
|
||||||
import type { Multiaddr } from "@multiformats/multiaddr";
|
import type { Multiaddr } from "@multiformats/multiaddr";
|
||||||
|
import { ENR } from "@waku/enr";
|
||||||
import type { Libp2p } from "libp2p";
|
import type { Libp2p } from "libp2p";
|
||||||
|
|
||||||
export enum Protocols {
|
export enum Protocols {
|
||||||
@ -11,6 +14,7 @@ export enum Protocols {
|
|||||||
Store = "store",
|
Store = "store",
|
||||||
LightPush = "lightpush",
|
LightPush = "lightpush",
|
||||||
Filter = "filter",
|
Filter = "filter",
|
||||||
|
PeerExchange = "peer-exchange",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointToPointProtocol {
|
export interface PointToPointProtocol {
|
||||||
@ -50,6 +54,25 @@ export interface LightPush extends PointToPointProtocol {
|
|||||||
) => Promise<SendResult>;
|
) => Promise<SendResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PeerExchange extends PointToPointProtocol {
|
||||||
|
query(
|
||||||
|
params: PeerExchangeQueryParams,
|
||||||
|
callback: (response: PeerExchangeResponse) => Promise<void> | void
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeQueryParams {
|
||||||
|
numPeers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeResponse {
|
||||||
|
peerInfos: PeerInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerInfo {
|
||||||
|
ENR?: ENR;
|
||||||
|
}
|
||||||
|
|
||||||
export enum PageDirection {
|
export enum PageDirection {
|
||||||
BACKWARD = "backward",
|
BACKWARD = "backward",
|
||||||
FORWARD = "forward",
|
FORWARD = "forward",
|
||||||
@ -60,6 +83,11 @@ export interface TimeFilter {
|
|||||||
endTime: Date;
|
endTime: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeComponents {
|
||||||
|
connectionManager: ConnectionManager;
|
||||||
|
peerStore: PeerStore;
|
||||||
|
registrar: Registrar;
|
||||||
|
}
|
||||||
export type Cursor = {
|
export type Cursor = {
|
||||||
digest?: Uint8Array;
|
digest?: Uint8Array;
|
||||||
senderTime?: bigint;
|
senderTime?: bigint;
|
||||||
@ -126,6 +154,7 @@ export interface Waku {
|
|||||||
store?: Store;
|
store?: Store;
|
||||||
filter?: Filter;
|
filter?: Filter;
|
||||||
lightPush?: LightPush;
|
lightPush?: LightPush;
|
||||||
|
peerExchange?: PeerExchange;
|
||||||
|
|
||||||
dial(peer: PeerId | Multiaddr, protocols?: Protocols[]): Promise<Stream>;
|
dial(peer: PeerId | Multiaddr, protocols?: Protocols[]): Promise<Stream>;
|
||||||
|
|
||||||
@ -141,6 +170,7 @@ export interface WakuLight extends Waku {
|
|||||||
store: Store;
|
store: Store;
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
lightPush: LightPush;
|
lightPush: LightPush;
|
||||||
|
peerExchange: PeerExchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WakuPrivacy extends Waku {
|
export interface WakuPrivacy extends Waku {
|
||||||
@ -148,6 +178,7 @@ export interface WakuPrivacy extends Waku {
|
|||||||
store: undefined;
|
store: undefined;
|
||||||
filter: undefined;
|
filter: undefined;
|
||||||
lightPush: undefined;
|
lightPush: undefined;
|
||||||
|
peerExchange: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WakuFull extends Waku {
|
export interface WakuFull extends Waku {
|
||||||
@ -155,6 +186,7 @@ export interface WakuFull extends Waku {
|
|||||||
store: Store;
|
store: Store;
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
lightPush: LightPush;
|
lightPush: LightPush;
|
||||||
|
peerExchange: PeerExchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RateLimitProof {
|
export interface RateLimitProof {
|
||||||
|
0
packages/peer-exchange/.eslintignore
Normal file
0
packages/peer-exchange/.eslintignore
Normal file
6
packages/peer-exchange/.eslintrc.cjs
Normal file
6
packages/peer-exchange/.eslintrc.cjs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: "./tsconfig.dev.json",
|
||||||
|
},
|
||||||
|
};
|
11
packages/peer-exchange/.mocharc.json
Normal file
11
packages/peer-exchange/.mocharc.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extension": ["ts"],
|
||||||
|
"spec": "src/**/*.spec.ts",
|
||||||
|
"require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
|
||||||
|
"loader": "ts-node/esm",
|
||||||
|
"node-option": [
|
||||||
|
"experimental-specifier-resolution=node",
|
||||||
|
"loader=ts-node/esm"
|
||||||
|
],
|
||||||
|
"exit": true
|
||||||
|
}
|
4
packages/peer-exchange/.prettierignore
Normal file
4
packages/peer-exchange/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
bundle
|
||||||
|
dist
|
||||||
|
node_modules
|
45
packages/peer-exchange/karma.conf.cjs
Normal file
45
packages/peer-exchange/karma.conf.cjs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
process.env.CHROME_BIN = require("puppeteer").executablePath();
|
||||||
|
const webpack = require("webpack");
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
frameworks: ["webpack", "mocha"],
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
|
preprocessors: {
|
||||||
|
"src/**/*.ts": ["webpack"],
|
||||||
|
},
|
||||||
|
envPreprocessor: ["CI"],
|
||||||
|
reporters: ["progress"],
|
||||||
|
browsers: ["ChromeHeadless"],
|
||||||
|
singleRun: true,
|
||||||
|
client: {
|
||||||
|
mocha: {
|
||||||
|
timeout: 6000, // Default is 2s
|
||||||
|
},
|
||||||
|
},
|
||||||
|
webpack: {
|
||||||
|
mode: "development",
|
||||||
|
module: {
|
||||||
|
rules: [{ test: /\.([cm]?ts|tsx)$/, loader: "ts-loader" }],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
"process.env.CI": process.env.CI || false,
|
||||||
|
}),
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
process: "process/browser.js",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: [".ts", ".tsx", ".js"],
|
||||||
|
extensionAlias: {
|
||||||
|
".js": [".js", ".ts"],
|
||||||
|
".cjs": [".cjs", ".cts"],
|
||||||
|
".mjs": [".mjs", ".mts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stats: { warnings: false },
|
||||||
|
devtool: "inline-source-map",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
103
packages/peer-exchange/package.json
Normal file
103
packages/peer-exchange/package.json
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"name": "@waku/peer-exchange",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Peer Exchange (https://rfc.vac.dev/spec/34/) protocol for Waku",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"author": "Waku Team",
|
||||||
|
"homepage": "https://github.com/waku-org/js-waku/tree/master/packages/peer-exchange#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/waku-org/js-waku.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/waku-org/js-waku/issues"
|
||||||
|
},
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"keywords": [
|
||||||
|
"waku",
|
||||||
|
"decentralized",
|
||||||
|
"secure",
|
||||||
|
"communication",
|
||||||
|
"web3",
|
||||||
|
"ethereum",
|
||||||
|
"dapps",
|
||||||
|
"privacy"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "run-s build:**",
|
||||||
|
"build:esm": "tsc",
|
||||||
|
"build:bundle": "rollup --config rollup.config.js",
|
||||||
|
"fix": "run-s fix:*",
|
||||||
|
"fix:prettier": "prettier . --write",
|
||||||
|
"fix:lint": "eslint src --ext .ts --ext .cjs --fix",
|
||||||
|
"check": "run-s check:*",
|
||||||
|
"check:lint": "eslint src --ext .ts",
|
||||||
|
"check:prettier": "prettier . --list-different",
|
||||||
|
"check:spelling": "cspell \"{README.md,src/**/*.ts}\"",
|
||||||
|
"check:tsc": "tsc -p tsconfig.dev.json",
|
||||||
|
"prepublish": "npm run build",
|
||||||
|
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@libp2p/interface-connection": "^3.0.3",
|
||||||
|
"@libp2p/interface-peer-discovery": "^1.0.0",
|
||||||
|
"@libp2p/interface-peer-discovery-compliance-tests": "^2.0.1",
|
||||||
|
"@libp2p/interface-peer-id": "^1.0.6",
|
||||||
|
"@libp2p/interface-peer-info": "^1.0.4",
|
||||||
|
"@libp2p/interface-peer-store": "^1.2.3",
|
||||||
|
"@waku/core": "*",
|
||||||
|
"@waku/enr": "*",
|
||||||
|
"@waku/interfaces": "*",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"it-all": "^1.0.6",
|
||||||
|
"it-length-prefixed": "^8.0.2",
|
||||||
|
"it-pipe": "^2.0.4",
|
||||||
|
"uint8arraylist": "^2.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@libp2p/peer-id-factory": "^1.0.15",
|
||||||
|
"@rollup/plugin-commonjs": "^22.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.8.1",
|
||||||
|
"@typescript-eslint/parser": "^5.8.1",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"cspell": "^5.14.0",
|
||||||
|
"eslint": "^8.6.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
|
"eslint-plugin-functional": "^4.0.2",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.1.1",
|
||||||
|
"rollup": "^2.75.0",
|
||||||
|
"ts-loader": "^9.4.1",
|
||||||
|
"typescript": "^4.6.3",
|
||||||
|
"uint8arrays": "^4.0.2"
|
||||||
|
},
|
||||||
|
"typedoc": {
|
||||||
|
"entryPoint": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"bundle",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"!**/*.spec.*",
|
||||||
|
"!**/*.json",
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
]
|
||||||
|
}
|
21
packages/peer-exchange/rollup.config.js
Normal file
21
packages/peer-exchange/rollup.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import json from "@rollup/plugin-json";
|
||||||
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: {
|
||||||
|
index: "dist/index.js",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
dir: "bundle",
|
||||||
|
format: "esm",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
nodeResolve({
|
||||||
|
browser: true,
|
||||||
|
preferBuiltins: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
1
packages/peer-exchange/src/index.ts
Normal file
1
packages/peer-exchange/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { wakuPeerExchange, PeerExchangeCodec } from "./waku_peer_exchange.js";
|
44
packages/peer-exchange/src/rpc.ts
Normal file
44
packages/peer-exchange/src/rpc.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { proto_peer_exchange as proto } from "@waku/proto";
|
||||||
|
import { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PeerExchangeRPC represents a message conforming to the Waku Peer Exchange protocol
|
||||||
|
*/
|
||||||
|
export class PeerExchangeRPC {
|
||||||
|
public constructor(public proto: proto.PeerExchangeRPC) {}
|
||||||
|
|
||||||
|
static createRequest(params: proto.PeerExchangeQuery): PeerExchangeRPC {
|
||||||
|
const { numPeers } = params;
|
||||||
|
return new PeerExchangeRPC({
|
||||||
|
query: {
|
||||||
|
numPeers: numPeers,
|
||||||
|
},
|
||||||
|
response: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the current PeerExchangeRPC request to bytes
|
||||||
|
* @returns Uint8Array
|
||||||
|
*/
|
||||||
|
encode(): Uint8Array {
|
||||||
|
return proto.PeerExchangeRPC.encode(this.proto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the current PeerExchangeRPC request to bytes
|
||||||
|
* @returns Uint8Array
|
||||||
|
*/
|
||||||
|
static decode(bytes: Uint8ArrayList): PeerExchangeRPC {
|
||||||
|
const res = proto.PeerExchangeRPC.decode(bytes);
|
||||||
|
return new PeerExchangeRPC(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
get query(): proto.PeerExchangeQuery | undefined {
|
||||||
|
return this.proto.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
get response(): proto.PeerExchangeResponse | undefined {
|
||||||
|
return this.proto.response;
|
||||||
|
}
|
||||||
|
}
|
135
packages/peer-exchange/src/waku_peer_exchange.ts
Normal file
135
packages/peer-exchange/src/waku_peer_exchange.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import type { Stream } from "@libp2p/interface-connection";
|
||||||
|
import type { PeerId } from "@libp2p/interface-peer-id";
|
||||||
|
import type { Peer, PeerStore } from "@libp2p/interface-peer-store";
|
||||||
|
import type { IncomingStreamData } from "@libp2p/interface-registrar";
|
||||||
|
import { ENR } from "@waku/enr";
|
||||||
|
import type {
|
||||||
|
PeerExchange,
|
||||||
|
PeerExchangeComponents,
|
||||||
|
PeerExchangeQueryParams,
|
||||||
|
PeerExchangeResponse,
|
||||||
|
ProtocolOptions,
|
||||||
|
} from "@waku/interfaces";
|
||||||
|
import {
|
||||||
|
getPeersForProtocol,
|
||||||
|
selectConnection,
|
||||||
|
selectPeerForProtocol,
|
||||||
|
} from "@waku/libp2p-utils";
|
||||||
|
import debug from "debug";
|
||||||
|
import all from "it-all";
|
||||||
|
import * as lp from "it-length-prefixed";
|
||||||
|
import { pipe } from "it-pipe";
|
||||||
|
|
||||||
|
import { PeerExchangeRPC } from "./rpc.js";
|
||||||
|
|
||||||
|
export const PeerExchangeCodec = "/vac/waku/peer-exchange/2.0.0-alpha1";
|
||||||
|
|
||||||
|
const log = debug("waku:peer-exchange");
|
||||||
|
|
||||||
|
export class WakuPeerExchange implements PeerExchange {
|
||||||
|
private callback:
|
||||||
|
| ((response: PeerExchangeResponse) => Promise<void>)
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public components: PeerExchangeComponents,
|
||||||
|
public createOptions?: ProtocolOptions
|
||||||
|
) {
|
||||||
|
this.components.registrar
|
||||||
|
.handle(PeerExchangeCodec, this.handler.bind(this))
|
||||||
|
.catch((e) => log("Failed to register peer exchange protocol", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(
|
||||||
|
params: PeerExchangeQueryParams,
|
||||||
|
callback: (response: PeerExchangeResponse) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
const { numPeers } = params;
|
||||||
|
|
||||||
|
const rpcQuery = PeerExchangeRPC.createRequest({
|
||||||
|
numPeers: BigInt(numPeers),
|
||||||
|
});
|
||||||
|
|
||||||
|
const peer = await this.getPeer();
|
||||||
|
|
||||||
|
const stream = await this.newStream(peer);
|
||||||
|
|
||||||
|
await pipe(
|
||||||
|
[rpcQuery.encode()],
|
||||||
|
lp.encode(),
|
||||||
|
stream,
|
||||||
|
lp.decode(),
|
||||||
|
async (source) => await all(source)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handler(streamData: IncomingStreamData): void {
|
||||||
|
const { stream } = streamData;
|
||||||
|
pipe(stream, lp.decode(), async (source) => {
|
||||||
|
for await (const bytes of source) {
|
||||||
|
const decoded = PeerExchangeRPC.decode(bytes).response;
|
||||||
|
|
||||||
|
if (!decoded) {
|
||||||
|
throw new Error("Failed to decode response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrs = await Promise.all(
|
||||||
|
decoded.peerInfos.map(
|
||||||
|
(peerInfo) => peerInfo.enr && ENR.decode(peerInfo.enr)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const peerInfos = enrs.map((enr) => {
|
||||||
|
return {
|
||||||
|
ENR: enr,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.callback) throw new Error("Callback not set");
|
||||||
|
|
||||||
|
await this.callback({ peerInfos });
|
||||||
|
}
|
||||||
|
}).catch((err) => log("Failed to handle peer exchange request", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPeer(peerId?: PeerId): Promise<Peer> {
|
||||||
|
const res = await selectPeerForProtocol(
|
||||||
|
this.components.peerStore,
|
||||||
|
[PeerExchangeCodec],
|
||||||
|
peerId
|
||||||
|
);
|
||||||
|
if (!res) {
|
||||||
|
throw new Error(`Failed to select peer for ${PeerExchangeCodec}`);
|
||||||
|
}
|
||||||
|
return res.peer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async newStream(peer: Peer): Promise<Stream> {
|
||||||
|
const connections = this.components.connectionManager.getConnections(
|
||||||
|
peer.id
|
||||||
|
);
|
||||||
|
const connection = selectConnection(connections);
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error("Failed to get a connection to the peer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection.newStream(PeerExchangeCodec);
|
||||||
|
}
|
||||||
|
|
||||||
|
async peers(): Promise<Peer[]> {
|
||||||
|
return getPeersForProtocol(this.components.peerStore, [PeerExchangeCodec]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get peerStore(): PeerStore {
|
||||||
|
return this.components.peerStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wakuPeerExchange(
|
||||||
|
init: Partial<ProtocolOptions> = {}
|
||||||
|
): (components: PeerExchangeComponents) => WakuPeerExchange {
|
||||||
|
return (components: PeerExchangeComponents) =>
|
||||||
|
new WakuPeerExchange(components, init);
|
||||||
|
}
|
112
packages/peer-exchange/src/waku_peer_exchange_discovery.ts
Normal file
112
packages/peer-exchange/src/waku_peer_exchange_discovery.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
PeerDiscovery,
|
||||||
|
PeerDiscoveryEvents,
|
||||||
|
symbol,
|
||||||
|
} from "@libp2p/interface-peer-discovery";
|
||||||
|
import { PeerInfo } from "@libp2p/interface-peer-info";
|
||||||
|
import { PeerProtocolsChangeData } from "@libp2p/interface-peer-store";
|
||||||
|
import { EventEmitter } from "@libp2p/interfaces/events";
|
||||||
|
import { PeerExchangeComponents } from "@waku/interfaces";
|
||||||
|
import debug from "debug";
|
||||||
|
|
||||||
|
import { PeerExchangeCodec } from "./waku_peer_exchange";
|
||||||
|
|
||||||
|
const log = debug("waku:peer-exchange-discovery");
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
/**
|
||||||
|
* Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap')
|
||||||
|
*/
|
||||||
|
tagName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bootstrap peer tag will have this value (default: 50)
|
||||||
|
*/
|
||||||
|
tagValue?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes)
|
||||||
|
*/
|
||||||
|
tagTTL?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BOOTSTRAP_TAG_NAME = "peer-exchange";
|
||||||
|
const DEFAULT_BOOTSTRAP_TAG_VALUE = 50;
|
||||||
|
const DEFAULT_BOOTSTRAP_TAG_TTL = 120000;
|
||||||
|
|
||||||
|
export class PeerExchangeDiscovery
|
||||||
|
extends EventEmitter<PeerDiscoveryEvents>
|
||||||
|
implements PeerDiscovery
|
||||||
|
{
|
||||||
|
private readonly components: PeerExchangeComponents;
|
||||||
|
private readonly options: Options;
|
||||||
|
private isStarted: boolean;
|
||||||
|
|
||||||
|
private readonly eventHandler = async (
|
||||||
|
event: CustomEvent<PeerProtocolsChangeData>
|
||||||
|
): Promise<void> => {
|
||||||
|
const { protocols } = event.detail;
|
||||||
|
if (!protocols.includes(PeerExchangeCodec)) return;
|
||||||
|
|
||||||
|
const { peerId } = event.detail;
|
||||||
|
const peer = await this.components.peerStore.get(peerId);
|
||||||
|
const peerInfo = {
|
||||||
|
id: peerId,
|
||||||
|
multiaddrs: peer.addresses.map((address) => address.multiaddr),
|
||||||
|
protocols: [],
|
||||||
|
};
|
||||||
|
await this.components.peerStore.tagPeer(
|
||||||
|
peerId,
|
||||||
|
DEFAULT_BOOTSTRAP_TAG_NAME,
|
||||||
|
{
|
||||||
|
value: this.options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
|
||||||
|
ttl: this.options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.dispatchEvent(new CustomEvent<PeerInfo>("peer", { detail: peerInfo }));
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(components: PeerExchangeComponents, options: Options = {}) {
|
||||||
|
super();
|
||||||
|
this.components = components;
|
||||||
|
this.options = options;
|
||||||
|
this.isStarted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start emitting events
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
if (this.isStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Starting peer exchange node discovery, discovering peers");
|
||||||
|
|
||||||
|
this.components.peerStore.addEventListener(
|
||||||
|
"change:protocols",
|
||||||
|
this.eventHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove event listener
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (!this.isStarted) return;
|
||||||
|
log("Stopping peer exchange node discovery");
|
||||||
|
this.isStarted = false;
|
||||||
|
this.components.peerStore.removeEventListener(
|
||||||
|
"change:protocols",
|
||||||
|
this.eventHandler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get [symbol](): true {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag](): string {
|
||||||
|
return "@waku/peer-exchange";
|
||||||
|
}
|
||||||
|
}
|
8
packages/peer-exchange/tsconfig.dev.json
Normal file
8
packages/peer-exchange/tsconfig.dev.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"exclude": []
|
||||||
|
}
|
54
packages/peer-exchange/tsconfig.json
Normal file
54
packages/peer-exchange/tsconfig.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": true,
|
||||||
|
"target": "es2020",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "es2020",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
"resolveJsonModule": true /* Include modules imported with .json extension. */,
|
||||||
|
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||||
|
"strictNullChecks": true /* Enable strict null checks. */,
|
||||||
|
"strictFunctionTypes": true /* Enable strict checking of function types. */,
|
||||||
|
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
|
||||||
|
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
|
||||||
|
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||||
|
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||||
|
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||||
|
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
||||||
|
/* Debugging Options */
|
||||||
|
"traceResolution": false /* Report module resolution log messages. */,
|
||||||
|
"listEmittedFiles": false /* Print names of generated files part of the compilation. */,
|
||||||
|
"listFiles": false /* Print names of files part of the compilation. */,
|
||||||
|
"pretty": true /* Stylize errors and messages using color and context. */,
|
||||||
|
|
||||||
|
// Due to broken types in indirect dependencies
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
||||||
|
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
|
||||||
|
|
||||||
|
"lib": ["es2020", "dom"],
|
||||||
|
"types": ["node", "mocha"],
|
||||||
|
"typeRoots": ["node_modules/@types", "src/types"]
|
||||||
|
},
|
||||||
|
"include": ["src", ".eslintrc.js"],
|
||||||
|
"exclude": ["src/**/*.spec.ts", "src/test_utils", "dist", "bundle"],
|
||||||
|
"compileOnSave": false,
|
||||||
|
"ts-node": {
|
||||||
|
"files": true
|
||||||
|
}
|
||||||
|
}
|
@ -9,3 +9,5 @@ export * as proto_lightpush from "./lib/light_push.js";
|
|||||||
export { PushResponse } from "./lib/light_push.js";
|
export { PushResponse } from "./lib/light_push.js";
|
||||||
|
|
||||||
export * as proto_store from "./lib/store.js";
|
export * as proto_store from "./lib/store.js";
|
||||||
|
|
||||||
|
export * as proto_peer_exchange from "./lib/peer_exchange.js";
|
||||||
|
18
packages/proto/src/lib/peer_exchange.proto
Normal file
18
packages/proto/src/lib/peer_exchange.proto
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
message PeerInfo {
|
||||||
|
optional bytes enr = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PeerExchangeQuery {
|
||||||
|
optional uint64 numPeers = 1; // number of peers requested
|
||||||
|
}
|
||||||
|
|
||||||
|
message PeerExchangeResponse {
|
||||||
|
repeated PeerInfo peerInfos = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PeerExchangeRPC {
|
||||||
|
optional PeerExchangeQuery query = 1;
|
||||||
|
optional PeerExchangeResponse response = 2;
|
||||||
|
}
|
271
packages/proto/src/lib/peer_exchange.ts
Normal file
271
packages/proto/src/lib/peer_exchange.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/* eslint-disable import/export */
|
||||||
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
|
|
||||||
|
import { encodeMessage, decodeMessage, message } from "protons-runtime";
|
||||||
|
import type { Uint8ArrayList } from "uint8arraylist";
|
||||||
|
import type { Codec } from "protons-runtime";
|
||||||
|
|
||||||
|
export interface PeerInfo {
|
||||||
|
enr?: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace PeerInfo {
|
||||||
|
let _codec: Codec<PeerInfo>;
|
||||||
|
|
||||||
|
export const codec = (): Codec<PeerInfo> => {
|
||||||
|
if (_codec == null) {
|
||||||
|
_codec = message<PeerInfo>(
|
||||||
|
(obj, writer, opts = {}) => {
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.fork();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.enr != null) {
|
||||||
|
writer.uint32(10);
|
||||||
|
writer.bytes(obj.enr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.ldelim();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(reader, length) => {
|
||||||
|
const obj: any = {};
|
||||||
|
|
||||||
|
const end = length == null ? reader.len : reader.pos + length;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
obj.enr = reader.bytes();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _codec;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encode = (obj: PeerInfo): Uint8Array => {
|
||||||
|
return encodeMessage(obj, PeerInfo.codec());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (buf: Uint8Array | Uint8ArrayList): PeerInfo => {
|
||||||
|
return decodeMessage(buf, PeerInfo.codec());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeQuery {
|
||||||
|
numPeers?: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace PeerExchangeQuery {
|
||||||
|
let _codec: Codec<PeerExchangeQuery>;
|
||||||
|
|
||||||
|
export const codec = (): Codec<PeerExchangeQuery> => {
|
||||||
|
if (_codec == null) {
|
||||||
|
_codec = message<PeerExchangeQuery>(
|
||||||
|
(obj, writer, opts = {}) => {
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.fork();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.numPeers != null) {
|
||||||
|
writer.uint32(8);
|
||||||
|
writer.uint64(obj.numPeers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.ldelim();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(reader, length) => {
|
||||||
|
const obj: any = {};
|
||||||
|
|
||||||
|
const end = length == null ? reader.len : reader.pos + length;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
obj.numPeers = reader.uint64();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _codec;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encode = (obj: PeerExchangeQuery): Uint8Array => {
|
||||||
|
return encodeMessage(obj, PeerExchangeQuery.codec());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (
|
||||||
|
buf: Uint8Array | Uint8ArrayList
|
||||||
|
): PeerExchangeQuery => {
|
||||||
|
return decodeMessage(buf, PeerExchangeQuery.codec());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeResponse {
|
||||||
|
peerInfos: PeerInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace PeerExchangeResponse {
|
||||||
|
let _codec: Codec<PeerExchangeResponse>;
|
||||||
|
|
||||||
|
export const codec = (): Codec<PeerExchangeResponse> => {
|
||||||
|
if (_codec == null) {
|
||||||
|
_codec = message<PeerExchangeResponse>(
|
||||||
|
(obj, writer, opts = {}) => {
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.fork();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.peerInfos != null) {
|
||||||
|
for (const value of obj.peerInfos) {
|
||||||
|
writer.uint32(10);
|
||||||
|
PeerInfo.codec().encode(value, writer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Protocol error: required field "peerInfos" was not found in object'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.ldelim();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(reader, length) => {
|
||||||
|
const obj: any = {
|
||||||
|
peerInfos: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = length == null ? reader.len : reader.pos + length;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
obj.peerInfos.push(
|
||||||
|
PeerInfo.codec().decode(reader, reader.uint32())
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _codec;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encode = (obj: PeerExchangeResponse): Uint8Array => {
|
||||||
|
return encodeMessage(obj, PeerExchangeResponse.codec());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (
|
||||||
|
buf: Uint8Array | Uint8ArrayList
|
||||||
|
): PeerExchangeResponse => {
|
||||||
|
return decodeMessage(buf, PeerExchangeResponse.codec());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerExchangeRPC {
|
||||||
|
query?: PeerExchangeQuery;
|
||||||
|
response?: PeerExchangeResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace PeerExchangeRPC {
|
||||||
|
let _codec: Codec<PeerExchangeRPC>;
|
||||||
|
|
||||||
|
export const codec = (): Codec<PeerExchangeRPC> => {
|
||||||
|
if (_codec == null) {
|
||||||
|
_codec = message<PeerExchangeRPC>(
|
||||||
|
(obj, writer, opts = {}) => {
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.fork();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.query != null) {
|
||||||
|
writer.uint32(10);
|
||||||
|
PeerExchangeQuery.codec().encode(obj.query, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.response != null) {
|
||||||
|
writer.uint32(18);
|
||||||
|
PeerExchangeResponse.codec().encode(obj.response, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.lengthDelimited !== false) {
|
||||||
|
writer.ldelim();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(reader, length) => {
|
||||||
|
const obj: any = {};
|
||||||
|
|
||||||
|
const end = length == null ? reader.len : reader.pos + length;
|
||||||
|
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1:
|
||||||
|
obj.query = PeerExchangeQuery.codec().decode(
|
||||||
|
reader,
|
||||||
|
reader.uint32()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
obj.response = PeerExchangeResponse.codec().decode(
|
||||||
|
reader,
|
||||||
|
reader.uint32()
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipType(tag & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _codec;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encode = (obj: PeerExchangeRPC): Uint8Array => {
|
||||||
|
return encodeMessage(obj, PeerExchangeRPC.codec());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decode = (buf: Uint8Array | Uint8ArrayList): PeerExchangeRPC => {
|
||||||
|
return decodeMessage(buf, PeerExchangeRPC.codec());
|
||||||
|
};
|
||||||
|
}
|
@ -47,6 +47,8 @@ export interface Args {
|
|||||||
lightpush?: boolean;
|
lightpush?: boolean;
|
||||||
filter?: boolean;
|
filter?: boolean;
|
||||||
store?: boolean;
|
store?: boolean;
|
||||||
|
peerExchange?: boolean;
|
||||||
|
discv5Discovery?: boolean;
|
||||||
storeMessageDbUrl?: string;
|
storeMessageDbUrl?: string;
|
||||||
topics?: string;
|
topics?: string;
|
||||||
rpcPrivate?: boolean;
|
rpcPrivate?: boolean;
|
||||||
@ -54,6 +56,7 @@ export interface Args {
|
|||||||
tcpPort?: number;
|
tcpPort?: number;
|
||||||
rpcPort?: number;
|
rpcPort?: number;
|
||||||
websocketPort?: number;
|
websocketPort?: number;
|
||||||
|
discv5BootstrapNode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
@ -167,10 +170,12 @@ export class Nwaku {
|
|||||||
process.env.WAKUNODE2_STORE_MESSAGE_DB_URL = "";
|
process.env.WAKUNODE2_STORE_MESSAGE_DB_URL = "";
|
||||||
|
|
||||||
const argsArray = argsToArray(mergedArgs);
|
const argsArray = argsToArray(mergedArgs);
|
||||||
|
|
||||||
if (WAKU_SERVICE_NODE_PARAMS) {
|
if (WAKU_SERVICE_NODE_PARAMS) {
|
||||||
argsArray.push(WAKU_SERVICE_NODE_PARAMS);
|
argsArray.push(WAKU_SERVICE_NODE_PARAMS);
|
||||||
}
|
}
|
||||||
log(`nwaku args: ${argsArray.join(" ")}`);
|
log(`nwaku args: ${argsArray.join(" ")}`);
|
||||||
|
|
||||||
this.process = spawn(WAKU_SERVICE_NODE_BIN, argsArray, {
|
this.process = spawn(WAKU_SERVICE_NODE_BIN, argsArray, {
|
||||||
cwd: WAKU_SERVICE_NODE_DIR,
|
cwd: WAKU_SERVICE_NODE_DIR,
|
||||||
stdio: [
|
stdio: [
|
||||||
|
@ -46,7 +46,7 @@ describe("Waku Message Ephemeral field", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.timeout(15000);
|
this.timeout(50_000);
|
||||||
nwaku = new Nwaku(makeLogFileName(this));
|
nwaku = new Nwaku(makeLogFileName(this));
|
||||||
await nwaku.start({ filter: true, lightpush: true, store: true });
|
await nwaku.start({ filter: true, lightpush: true, store: true });
|
||||||
waku = await createLightNode({
|
waku = await createLightNode({
|
||||||
@ -55,11 +55,16 @@ describe("Waku Message Ephemeral field", () => {
|
|||||||
});
|
});
|
||||||
await waku.start();
|
await waku.start();
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku);
|
|
||||||
|
await waitForRemotePeer(waku, [
|
||||||
|
Protocols.Filter,
|
||||||
|
Protocols.LightPush,
|
||||||
|
Protocols.Store,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Ephemeral messages are not stored", async function () {
|
it("Ephemeral messages are not stored", async function () {
|
||||||
this.timeout(15_000);
|
this.timeout(50_000);
|
||||||
|
|
||||||
const asymText =
|
const asymText =
|
||||||
"This message is encrypted for me using asymmetric encryption";
|
"This message is encrypted for me using asymmetric encryption";
|
||||||
|
60
packages/tests/tests/peer_exchange.node.spec.ts
Normal file
60
packages/tests/tests/peer_exchange.node.spec.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { bootstrap } from "@libp2p/bootstrap";
|
||||||
|
import { waitForRemotePeer } from "@waku/core";
|
||||||
|
import {
|
||||||
|
Fleet,
|
||||||
|
getPredefinedBootstrapNodes,
|
||||||
|
} from "@waku/core/lib/predefined_bootstrap_nodes";
|
||||||
|
import { createLightNode } from "@waku/create";
|
||||||
|
import type { PeerExchangeResponse, WakuFull } from "@waku/interfaces";
|
||||||
|
import { Protocols } from "@waku/interfaces";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
describe("Peer Exchange: Node", () => {
|
||||||
|
let waku: WakuFull;
|
||||||
|
afterEach(async function () {
|
||||||
|
!!waku && waku.stop().catch((e) => console.log("Waku failed to stop", e));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test Fleet: Queries successfully [Live Data]", async function () {
|
||||||
|
this.timeout(150_000);
|
||||||
|
|
||||||
|
// skipping in CI as this test demonstrates Peer Exchange working with the test fleet
|
||||||
|
// but not with locally run nwaku nodes
|
||||||
|
if (process.env.ci) {
|
||||||
|
this.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
const waku = await createLightNode({
|
||||||
|
libp2p: {
|
||||||
|
peerDiscovery: [
|
||||||
|
bootstrap({ list: getPredefinedBootstrapNodes(Fleet.Test) }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waku.start();
|
||||||
|
|
||||||
|
await waitForRemotePeer(waku, [Protocols.PeerExchange]);
|
||||||
|
|
||||||
|
let receivedCallback = false;
|
||||||
|
const numPeersToRequest = 3;
|
||||||
|
const callback = (response: PeerExchangeResponse): void => {
|
||||||
|
receivedCallback = true;
|
||||||
|
expect(response.peerInfos.length).to.be.greaterThan(0);
|
||||||
|
expect(response.peerInfos.length).to.be.lessThanOrEqual(
|
||||||
|
numPeersToRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.peerInfos[0].ENR).to.not.be.null;
|
||||||
|
};
|
||||||
|
|
||||||
|
await waku.peerExchange.query(
|
||||||
|
{
|
||||||
|
numPeers: numPeersToRequest,
|
||||||
|
},
|
||||||
|
callback
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(receivedCallback).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
@ -342,7 +342,7 @@ describe("Waku Relay [node only]", () => {
|
|||||||
await waku.start();
|
await waku.start();
|
||||||
|
|
||||||
nwaku = new Nwaku(this.test?.ctx?.currentTest?.title + "");
|
nwaku = new Nwaku(this.test?.ctx?.currentTest?.title + "");
|
||||||
await nwaku.start();
|
await nwaku.start({ relay: true });
|
||||||
|
|
||||||
await waku.dial(await nwaku.getMultiaddrWithId());
|
await waku.dial(await nwaku.getMultiaddrWithId());
|
||||||
await waitForRemotePeer(waku, [Protocols.Relay]);
|
await waitForRemotePeer(waku, [Protocols.Relay]);
|
||||||
|
@ -218,7 +218,11 @@ describe("Wait for remote peer", function () {
|
|||||||
});
|
});
|
||||||
await waku2.start();
|
await waku2.start();
|
||||||
await waku2.dial(multiAddrWithId);
|
await waku2.dial(multiAddrWithId);
|
||||||
await waitForRemotePeer(waku2);
|
await waitForRemotePeer(waku2, [
|
||||||
|
Protocols.Filter,
|
||||||
|
Protocols.Store,
|
||||||
|
Protocols.LightPush,
|
||||||
|
]);
|
||||||
|
|
||||||
const filterPeers = (await waku2.filter.peers()).map((peer) =>
|
const filterPeers = (await waku2.filter.peers()).map((peer) =>
|
||||||
peer.id.toString()
|
peer.id.toString()
|
||||||
|
@ -51,7 +51,11 @@ describe("Waku Dial [node only]", function () {
|
|||||||
});
|
});
|
||||||
await waku.start();
|
await waku.start();
|
||||||
await waku.dial(multiAddrWithId);
|
await waku.dial(multiAddrWithId);
|
||||||
await waitForRemotePeer(waku);
|
await waitForRemotePeer(waku, [
|
||||||
|
Protocols.Store,
|
||||||
|
Protocols.Filter,
|
||||||
|
Protocols.LightPush,
|
||||||
|
]);
|
||||||
|
|
||||||
const nimPeerId = await nwaku.getPeerId();
|
const nimPeerId = await nwaku.getPeerId();
|
||||||
expect(await waku.libp2p.peerStore.has(nimPeerId)).to.be.true;
|
expect(await waku.libp2p.peerStore.has(nimPeerId)).to.be.true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user