From 140791cc91da54ce37a97bbdae0af46cf5e22279 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Tue, 10 Aug 2021 11:32:14 +1000 Subject: [PATCH] Provide easy way to bootstrap when creating Waku node --- .cspell.json | 1 + CHANGELOG.md | 6 +- README.md | 27 +----- examples/eth-dm/src/waku.ts | 11 +-- examples/eth-pm-wallet-encryption/src/waku.ts | 11 +-- examples/store-reactjs-chat/src/App.js | 37 ++------- examples/web-chat/src/App.tsx | 14 ++-- guides/store-retrieve-messages.md | 24 +++--- package-lock.json | 60 ++++++++++++++ package.json | 1 + src/lib/discovery.ts | 2 + src/lib/waku.spec.ts | 82 ++++++++++++++++++- src/lib/waku.ts | 51 ++++++++++++ 13 files changed, 227 insertions(+), 100 deletions(-) diff --git a/.cspell.json b/.cspell.json index 90db492bb2..499e87d912 100644 --- a/.cspell.json +++ b/.cspell.json @@ -47,6 +47,7 @@ "livechat", "mkdir", "multiaddr", + "multiaddresses", "multiaddrs", "multicodecs", "mplex", diff --git a/CHANGELOG.md b/CHANGELOG.md index b439926b7f..cfbc8032bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- New `bootstrap` option for `Waku.create` to easily connect to Waku nodes upon start up. + ### Changed -- Renamed `discover.getStatusFleetNodes` to `discovery.getBootstrapNodes` and make it more generic to allow retrieval of bootstrap nodes from other sources. +- Renamed `discover.getStatusFleetNodes` to `discovery.getBootstrapNodes`; + Changed the API to allow retrieval of bootstrap nodes from other sources. ### Removed - Examples (cli-chat): The focus of this library is Web environment; diff --git a/README.md b/README.md index 5d502bdfdf..91780aef5b 100644 --- a/README.md +++ b/README.md @@ -32,32 +32,7 @@ npm install js-waku ```ts import { Waku } from 'js-waku'; -const waku = await Waku.create(); -``` - -### Connect to a new peer - -```ts -// Directly dial a new peer -await waku.dial('/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ'); - -// Or, add peer to address book so it auto dials in the background -waku.addPeerToAddressBook( - '16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ', - ['/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss'] -); -``` - -You can also use `getBootstrapNodes` to connect to Waku bootstrap nodes: - -```ts -import { getBootstrapNodes } from 'js-waku'; - -getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); -}); +const waku = await Waku.create({ bootstrap: true }); ``` ### Listen for messages diff --git a/examples/eth-dm/src/waku.ts b/examples/eth-dm/src/waku.ts index 5d24ab60a1..83214e1457 100644 --- a/examples/eth-dm/src/waku.ts +++ b/examples/eth-dm/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; +import { Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -9,14 +9,7 @@ export const PublicKeyContentTopic = '/eth-dm/1/public-key/proto'; export const DirectMessageContentTopic = '/eth-dm/1/direct-message/proto'; export async function initWaku(): Promise { - const waku = await Waku.create({}); - - // Dial all nodes it can find - getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); - }); + const waku = await Waku.create({ bootstrap: true }); // Wait to be connected to at least one peer await new Promise((resolve, reject) => { diff --git a/examples/eth-pm-wallet-encryption/src/waku.ts b/examples/eth-pm-wallet-encryption/src/waku.ts index 07c3bd1ffa..c85649276c 100644 --- a/examples/eth-pm-wallet-encryption/src/waku.ts +++ b/examples/eth-pm-wallet-encryption/src/waku.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { getBootstrapNodes, Waku, WakuMessage } from 'js-waku'; +import { Waku, WakuMessage } from 'js-waku'; import { DirectMessage, PublicKeyMessage } from './messaging/wire'; import { validatePublicKeyMessage } from './crypto'; import { Message } from './messaging/Messages'; @@ -11,14 +11,7 @@ export const DirectMessageContentTopic = '/eth-pm-wallet/1/direct-message/proto'; export async function initWaku(): Promise { - const waku = await Waku.create({}); - - // Dial all nodes it can find - getBootstrapNodes().then((nodes) => { - nodes.forEach((addr) => { - waku.dial(addr); - }); - }); + const waku = await Waku.create({ bootstrap: true }); // Wait to be connected to at least one peer await new Promise((resolve, reject) => { diff --git a/examples/store-reactjs-chat/src/App.js b/examples/store-reactjs-chat/src/App.js index fe9551393d..fbb206b2dc 100644 --- a/examples/store-reactjs-chat/src/App.js +++ b/examples/store-reactjs-chat/src/App.js @@ -1,5 +1,5 @@ import './App.css'; -import { getBootstrapNodes, StoreCodec, Waku } from 'js-waku'; +import { StoreCodec, Waku } from 'js-waku'; import * as React from 'react'; import protons from 'protons'; @@ -17,9 +17,6 @@ function App() { const [waku, setWaku] = React.useState(undefined); const [wakuStatus, setWakuStatus] = React.useState('None'); const [messages, setMessages] = React.useState([]); - // Set to true when Waku connects to a store node - // it does not reflect whether we then disconnected from said node. - const [connectedToStore, setConnectedToStore] = React.useState(false); React.useEffect(() => { if (!!waku) return; @@ -27,20 +24,15 @@ function App() { setWakuStatus('Starting'); - Waku.create().then((waku) => { + Waku.create({ bootstrap: true }).then((waku) => { setWaku(waku); setWakuStatus('Connecting'); - bootstrapWaku(waku).then(() => { - setWakuStatus('Ready'); - }); }); }, [waku, wakuStatus]); React.useEffect(() => { if (!waku) return; - // This is superfluous as the try/catch block would catch the failure if - // we are indeed not connected to any store node. - if (!connectedToStore) return; + if (wakuStatus !== 'Connected to Store') return; const interval = setInterval(() => { waku.store @@ -57,33 +49,27 @@ function App() { }, 10000); return () => clearInterval(interval); - }, [waku, connectedToStore]); + }, [waku, wakuStatus]); React.useEffect(() => { if (!waku) return; // We do not handle disconnection/re-connection in this example - if (connectedToStore) return; + if (wakuStatus === 'Connected to Store') return; const isStoreNode = ({ protocols }) => { if (protocols.includes(StoreCodec)) { // We are now connected to a store node - setConnectedToStore(true); + setWakuStatus('Connected to Store'); } }; - // This demonstrates how to wait for a connection to a store node. - // - // This is only for demonstration purposes. It is not really needed in this - // example app as we query the store node every 10s and catch if it fails. - // Meaning if we are not connected to a store node, then it just fails and - // we try again 10s later. waku.libp2p.peerStore.on('change:protocols', isStoreNode); return () => { waku.libp2p.peerStore.removeListener('change:protocols', isStoreNode); }; - }, [waku, connectedToStore]); + }, [waku, wakuStatus]); return (
@@ -100,15 +86,6 @@ function App() { export default App; -async function bootstrapWaku(waku) { - try { - const nodes = await getBootstrapNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); - } catch (e) { - console.error('Failed to bootstrap to Waku network'); - } -} - function decodeMessage(wakuMessage) { if (!wakuMessage.payload) return; diff --git a/examples/web-chat/src/App.tsx b/examples/web-chat/src/App.tsx index 3814d09bbd..fdc292e938 100644 --- a/examples/web-chat/src/App.tsx +++ b/examples/web-chat/src/App.tsx @@ -82,8 +82,10 @@ export default function App() { const persistedNick = window.localStorage.getItem('nick'); return persistedNick !== null ? persistedNick : generate(); }); - const [historicalMessagesRetrieved, setHistoricalMessagesRetrieved] = - useState(false); + const [ + historicalMessagesRetrieved, + setHistoricalMessagesRetrieved, + ] = useState(false); useEffect(() => { localStorage.setItem('nick', nick); @@ -179,16 +181,10 @@ async function initWaku(setter: (waku: Waku) => void) { }, }, }, + bootstrap: getBootstrapNodes.bind({}, selectFleetEnv()), }); setter(waku); - - const nodes = await getBootstrapNodes(selectFleetEnv()); - await Promise.all( - nodes.map((addr) => { - return waku.dial(addr); - }) - ); } catch (e) { console.log('Issue starting waku ', e); } diff --git a/guides/store-retrieve-messages.md b/guides/store-retrieve-messages.md index acd5f71551..fc1672c922 100644 --- a/guides/store-retrieve-messages.md +++ b/guides/store-retrieve-messages.md @@ -38,25 +38,21 @@ In order to interact with the Waku network, you first need a Waku instance: ```js import { Waku } from 'js-waku'; -const wakuNode = await Waku.create(); +const wakuNode = await Waku.create({ bootstrap: true }); ``` -# Connect to Other Peers - -The Waku instance needs to connect to other peers to communicate with the network. -You are free to choose other methods to bootstrap and DappConnect will ship with new bootstrap mechanisms in the future. - -For now, the easiest way is to connect to Waku bootstrap nodes: +Passing the `bootstrap` option will connect your node to predefined Waku nodes hosted by Status. +If you want to bootstrap to your own nodes, you can pass an array of multiaddresses instead: ```js -import { getBootstrapNodes } from 'js-waku'; +import { Waku } from 'js-waku'; -try { - const nodes = await getBootstrapNodes(); - await Promise.all(nodes.map((addr) => waku.dial(addr))); -} catch (e) { - console.error('Failed to bootstrap to Waku network'); -} +const wakuNode = await Waku.create({ + bootstrap: [ + '/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm', + '/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ' + ] +}); ``` # Use Protobuf diff --git a/package-lock.json b/package-lock.json index a2ea0fe611..d50285af27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "it-length-prefixed": "^5.0.2", "js-sha3": "^0.8.0", "libp2p": "^0.32.0", + "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.4", "libp2p-noise": "^4.0.0", @@ -15055,6 +15056,39 @@ "node": ">=14.0.0" } }, + "node_modules/libp2p-bootstrap": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/libp2p-bootstrap/-/libp2p-bootstrap-0.13.0.tgz", + "integrity": "sha512-8sXEZrikY+chKvMorkvOi9E/v9GvwsYr9DAEfzQZrOKQZByqhan1aXQKWrSpc4AxEv5/UopRzu1P47bkOi8wdw==", + "dependencies": { + "debug": "^4.3.1", + "mafmt": "^10.0.0", + "multiaddr": "^10.0.0", + "peer-id": "^0.15.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/libp2p-bootstrap/node_modules/peer-id": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.15.2.tgz", + "integrity": "sha512-3OMbup76F28gKsQK4rGheEJHwosnJGe2+Obsf1xFaS9DpUaG9/JK0rtguWVLbrkxPclsCceci8g3/ulg8jsORA==", + "dependencies": { + "class-is": "^1.1.0", + "libp2p-crypto": "^0.19.0", + "minimist": "^1.2.5", + "multiformats": "^9.3.0", + "protobufjs": "^6.10.2", + "uint8arrays": "^2.0.5" + }, + "bin": { + "peer-id": "src/bin.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/libp2p-crypto": { "version": "0.19.6", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.19.6.tgz", @@ -36622,6 +36656,32 @@ } } }, + "libp2p-bootstrap": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/libp2p-bootstrap/-/libp2p-bootstrap-0.13.0.tgz", + "integrity": "sha512-8sXEZrikY+chKvMorkvOi9E/v9GvwsYr9DAEfzQZrOKQZByqhan1aXQKWrSpc4AxEv5/UopRzu1P47bkOi8wdw==", + "requires": { + "debug": "^4.3.1", + "mafmt": "^10.0.0", + "multiaddr": "^10.0.0", + "peer-id": "^0.15.0" + }, + "dependencies": { + "peer-id": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/peer-id/-/peer-id-0.15.2.tgz", + "integrity": "sha512-3OMbup76F28gKsQK4rGheEJHwosnJGe2+Obsf1xFaS9DpUaG9/JK0rtguWVLbrkxPclsCceci8g3/ulg8jsORA==", + "requires": { + "class-is": "^1.1.0", + "libp2p-crypto": "^0.19.0", + "minimist": "^1.2.5", + "multiformats": "^9.3.0", + "protobufjs": "^6.10.2", + "uint8arrays": "^2.0.5" + } + } + } + }, "libp2p-crypto": { "version": "0.19.6", "resolved": "https://registry.npmjs.org/libp2p-crypto/-/libp2p-crypto-0.19.6.tgz", diff --git a/package.json b/package.json index b6f3799250..3e45650197 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "it-length-prefixed": "^5.0.2", "js-sha3": "^0.8.0", "libp2p": "^0.32.0", + "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.10.0", "libp2p-mplex": "^0.10.4", "libp2p-noise": "^4.0.0", diff --git a/src/lib/discovery.ts b/src/lib/discovery.ts index 6b75947423..f631909922 100644 --- a/src/lib/discovery.ts +++ b/src/lib/discovery.ts @@ -6,6 +6,8 @@ const dbg = debug('waku:discovery'); /** * GET list of nodes from remote HTTP host. * + * Default behaviour is to return nodes hosted by Status. + * * @param path The property path to access the node list. The result should be * a string, a string array or an object. If the result is an object then the * values of the objects are used as multiaddresses. diff --git a/src/lib/waku.spec.ts b/src/lib/waku.spec.ts index bad7599e33..797700bf3d 100644 --- a/src/lib/waku.spec.ts +++ b/src/lib/waku.spec.ts @@ -2,20 +2,98 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available import TCP from 'libp2p-tcp'; +import PeerId from 'peer-id'; -import { makeLogFileName, NimWaku, NOISE_KEY_1 } from '../test_utils/'; +import { + makeLogFileName, + NimWaku, + NOISE_KEY_1, + NOISE_KEY_2, +} from '../test_utils/'; import { Waku } from './waku'; describe('Waku Dial', function () { let waku: Waku; + let waku2: Waku; let nimWaku: NimWaku; afterEach(async function () { this.timeout(10_000); nimWaku ? nimWaku.stop() : null; - waku ? await waku.stop() : null; + + await Promise.all([waku ? waku.stop() : null, waku2 ? waku2.stop() : null]); + }); + + describe('Bootstrap', function () { + it('Passing an array', async function () { + this.timeout(10_000); + + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); + + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + + waku2 = await Waku.create({ + staticNoiseKey: NOISE_KEY_2, + libp2p: { + modules: { transport: [TCP] }, + }, + bootstrap: [multiAddrWithId], + }); + + const connectedPeerID: PeerId = await new Promise((resolve) => { + waku.libp2p.connectionManager.on('peer:connect', (connection) => { + resolve(connection.remotePeer); + }); + }); + + expect(connectedPeerID.toB58String()).to.eq( + waku2.libp2p.peerId.toB58String() + ); + }); + }); + + describe('Bootstrap', function () { + it('Passing a function', async function () { + this.timeout(10_000); + + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); + + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + + waku2 = await Waku.create({ + staticNoiseKey: NOISE_KEY_2, + libp2p: { + modules: { transport: [TCP] }, + }, + bootstrap: () => { + return [multiAddrWithId]; + }, + }); + + const connectedPeerID: PeerId = await new Promise((resolve) => { + waku.libp2p.connectionManager.on('peer:connect', (connection) => { + resolve(connection.remotePeer); + }); + }); + + expect(connectedPeerID.toB58String()).to.eq( + waku2.libp2p.peerId.toB58String() + ); + }); }); describe('Interop: Nim', function () { diff --git a/src/lib/waku.ts b/src/lib/waku.ts index 49c813cd5b..1108dd94f4 100644 --- a/src/lib/waku.ts +++ b/src/lib/waku.ts @@ -1,4 +1,6 @@ +import debug from 'debug'; import Libp2p, { Connection, Libp2pModules, Libp2pOptions } from 'libp2p'; +import Bootstrap from 'libp2p-bootstrap'; import { MuxedStream } from 'libp2p-interfaces/dist/src/stream-muxer/types'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available @@ -15,6 +17,7 @@ import Ping from 'libp2p/src/ping'; import { Multiaddr, multiaddr } from 'multiaddr'; import PeerId from 'peer-id'; +import { getBootstrapNodes } from './discovery'; import { WakuLightPush } from './waku_light_push'; import { WakuMessage } from './waku_message'; import { RelayCodecs, WakuRelay } from './waku_relay'; @@ -26,6 +29,8 @@ const websocketsTransportKey = Websockets.prototype[Symbol.toStringTag]; const DefaultPingKeepAliveValueSecs = 0; const DefaultRelayKeepAliveValueSecs = 5 * 60; +const dbg = debug('waku:waku'); + export interface CreateOptions { /** * The PubSub Topic to use. Defaults to {@link DefaultPubsubTopic}. @@ -71,6 +76,18 @@ export interface CreateOptions { * This is only used for test purposes to not run out of entropy during CI runs. */ staticNoiseKey?: bytes; + /** + * Use libp2p-bootstrap to discover and connect to new nodes. + * + * You can pass: + * - `true` to use {@link getBootstrapNodes}, + * - an array of multiaddresses, + * - a function that returns an array of multiaddresses (or Promise of). + * + * Note: It overrides any other peerDiscovery modules that may have been set via + * {@link CreateOptions.libp2p}. + */ + bootstrap?: boolean | string[] | (() => string[] | Promise); } export class Waku { @@ -161,6 +178,40 @@ export class Waku { pubsub: WakuRelay, }); + if (options?.bootstrap) { + let bootstrap: undefined | (() => string[] | Promise); + + if (options.bootstrap === true) { + bootstrap = getBootstrapNodes; + } else if (Array.isArray(options.bootstrap)) { + bootstrap = (): string[] => { + return options.bootstrap as string[]; + }; + } else if (typeof options.bootstrap === 'function') { + bootstrap = options.bootstrap; + } + + if (bootstrap !== undefined) { + // Note: this overrides any other peer discover + libp2pOpts.modules = Object.assign(libp2pOpts.modules, { + peerDiscovery: [Bootstrap], + }); + + try { + const list = await bootstrap(); + + libp2pOpts.config.peerDiscovery = { + [Bootstrap.tag]: { + list, + enabled: true, + }, + }; + } catch (e) { + dbg('Failed to retrieve bootstrap nodes', e); + } + } + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: modules property is correctly set thanks to voodoo const libp2p = await Libp2p.create(libp2pOpts);