Merge pull request #404 from status-im/eip-1459-2

This commit is contained in:
Franck R 2022-01-14 13:10:01 +11:00 committed by GitHub
commit f9d066252c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1096 additions and 103 deletions

View File

@ -23,8 +23,9 @@
"Dscore",
"ecies",
"editorconfig",
"ENR",
"ENRs",
"enr",
"enrs",
"enrtree",
"ephem",
"esnext",
"ethersproject",
@ -61,6 +62,7 @@
"muxer",
"mvps",
"nodekey",
"opendns",
"peerhave",
"prettierignore",
"proto",
@ -71,9 +73,11 @@
"rlnrelay",
"roadmap",
"sandboxed",
"scanf",
"secio",
"seckey",
"secp",
"sscanf",
"staticnode",
"statusim",
"submodule",

View File

@ -90,3 +90,42 @@ jobs:
with:
name: nim-waku-logs
path: log/
# Run tests that use live data or depend on external systems
# This should not be mandatory as part of the PR process to not have
# a blocker because said external system is down.
build_and_test_live_data:
strategy:
matrix:
node: [16]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2.3.3
- name: Install NodeJS
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- name: Cache npm cache
uses: actions/cache@v2
with:
path: ~/.npm
key: node-${{ matrix.os }}-${{ matrix.node }}-v1-${{ hashFiles('**/package-lock.json') }}
- name: install using npm ci
uses: bahmutov/npm-install@v1
- name: karma live data tests
env:
DEBUG: "waku:test*"
run: npm run test:karma-live-data
- name: Upload logs on failure
uses: actions/upload-artifact@v2
if: failure()
with:
name: nim-waku-logs
path: log/

View File

@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Implement DNS Discovery as per [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459),
with ENR records as defined in [31/WAKU2-ENR](https://rfc.vac.dev/spec/31/);
Available by passing `{ bootstrap: { enrUrl: enrtree://... } }` to `Waku.create`.
### Changed
- Test: Upgrade nim-waku node to v0.6.
- **Breaking**: Renamed `getBootstrapNodes` to `getNodesFromHostedJson`.
- Minimum node version changed to 16.
- **Breaking**: Changed `Waku.create` bootstrap option from `{ bootstrap: boolean }` to `{ bootstrap: BootstrapOptions }`.
Replace `{ boostrap: true }` with `{ boostrap: { default: true } }` to retain same behaviour.
### Fixed

View File

@ -6440,9 +6440,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -27967,9 +27967,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"capture-exit": {
"version": "2.0.0",

View File

@ -11,7 +11,7 @@ export const PrivateMessageContentTopic =
'/eth-pm-wallet/1/private-message/proto';
export async function initWaku(): Promise<Waku> {
const waku = await Waku.create({ bootstrap: true });
const waku = await Waku.create({ bootstrap: { default: true } });
// Wait to be connected to at least one peer
await new Promise((resolve, reject) => {

View File

@ -6440,9 +6440,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -27967,9 +27967,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"capture-exit": {
"version": "2.0.0",

View File

@ -9,7 +9,7 @@ export const PublicKeyContentTopic = '/eth-pm/1/public-key/proto';
export const PrivateMessageContentTopic = '/eth-pm/1/private-message/proto';
export async function initWaku(): Promise<Waku> {
const waku = await Waku.create({ bootstrap: true });
const waku = await Waku.create({ bootstrap: { default: true } });
// Wait to be connected to at least one peer
await new Promise((resolve, reject) => {

View File

@ -4858,9 +4858,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001296",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz",
"integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -20062,9 +20062,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001296",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz",
"integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",

View File

@ -5383,9 +5383,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001298",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz",
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -22888,9 +22888,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001298",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001298.tgz",
"integrity": "sha512-AcKqikjMLlvghZL/vfTHorlQsLDhGRalYf1+GmWCf5SCMziSGjRYQW/JEksj14NaYHIR6KIhrFAy0HV5C25UzQ=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",

View File

@ -6963,9 +6963,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001296",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz",
"integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -27344,9 +27344,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001296",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz",
"integrity": "sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",

View File

@ -1,6 +1,11 @@
import { useEffect, useReducer, useState } from 'react';
import './App.css';
import { PageDirection, getBootstrapNodes, Waku, WakuMessage } from 'js-waku';
import {
PageDirection,
getNodesFromHostedJson,
Waku,
WakuMessage,
} from 'js-waku';
import handleCommand from './command';
import Room from './Room';
import { WakuContext } from './WakuContext';
@ -175,7 +180,9 @@ async function initWaku(setter: (waku: Waku) => void) {
},
},
},
bootstrap: getBootstrapNodes.bind({}, selectFleetEnv()),
bootstrap: {
getPeers: getNodesFromHostedJson.bind({}, selectFleetEnv()),
},
});
setter(waku);

16
karma-live-data.conf.js Normal file
View File

@ -0,0 +1,16 @@
// import settings from default config file
let properties = null;
const originalConfigFn = require('./karma.conf.js');
originalConfigFn({
set: function (arg) {
properties = arg;
},
});
// pass `--grep '[live data]'` to mocha to only run live data tests
properties.client.args = ['--grep', '[live data]]'];
// export settings
module.exports = function (config) {
config.set(properties);
};

113
package-lock.json generated
View File

@ -14,7 +14,9 @@
"base64url": "^3.0.1",
"bigint-buffer": "^1.1.5",
"debug": "^4.3.1",
"dns-query": "^0.8.0",
"ecies-geth": "^1.5.2",
"hi-base32": "^0.5.1",
"it-concat": "^2.0.0",
"it-length-prefixed": "^5.0.2",
"js-sha3": "^0.8.0",
@ -2106,6 +2108,11 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz",
"integrity": "sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg=="
},
"node_modules/@motrix/nat-api": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@motrix/nat-api/-/nat-api-0.3.2.tgz",
@ -3118,6 +3125,14 @@
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
},
"node_modules/@types/dns-packet": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.2.4.tgz",
"integrity": "sha512-OAruArypdNxR/tzbmrtoyEuXeNTLaZCpO19BXaNC10T5ACIbvjmvhmV2RDEy2eLc3w8IjK7SY3cvUCcAW+sfoQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/eslint": {
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz",
@ -4864,9 +4879,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -6183,6 +6198,42 @@
"receptacle": "^1.3.2"
}
},
"node_modules/dns-packet": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.3.1.tgz",
"integrity": "sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==",
"dependencies": {
"@leichtgewicht/ip-codec": "^2.0.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/dns-query": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/dns-query/-/dns-query-0.8.0.tgz",
"integrity": "sha512-Gx3jYhdj9oLMZFieinpwpTFK0c2Q+teV53Se1+l4AbcWLPMUCBACu7qcj0IqTWwnpasWl8Gwgxeqw2RjoCwIoA==",
"dependencies": {
"@leichtgewicht/ip-codec": "^2.0.2",
"@types/dns-packet": "^5.2.0",
"dns-packet": "^5.3.0",
"dns-socket": "^4.2.2"
},
"bin": {
"dns-query": "bin/dns-query"
}
},
"node_modules/dns-socket": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.2.tgz",
"integrity": "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==",
"dependencies": {
"dns-packet": "^5.2.4"
},
"engines": {
"node": ">=6"
}
},
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -8368,6 +8419,11 @@
"node": ">= 8"
}
},
"node_modules/hi-base32": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz",
"integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA=="
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -19349,6 +19405,11 @@
"chalk": "^4.0.0"
}
},
"@leichtgewicht/ip-codec": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz",
"integrity": "sha512-nkalE/f1RvRGChwBnEIoBfSEYOXnCRdleKuv6+lePbMDrMZXeDQnqak5XDOeBgrPPyPfAdcCu/B5z+v3VhplGg=="
},
"@motrix/nat-api": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@motrix/nat-api/-/nat-api-0.3.2.tgz",
@ -20233,6 +20294,14 @@
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
"integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ=="
},
"@types/dns-packet": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@types/dns-packet/-/dns-packet-5.2.4.tgz",
"integrity": "sha512-OAruArypdNxR/tzbmrtoyEuXeNTLaZCpO19BXaNC10T5ACIbvjmvhmV2RDEy2eLc3w8IjK7SY3cvUCcAW+sfoQ==",
"requires": {
"@types/node": "*"
}
},
"@types/eslint": {
"version": "7.28.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.1.tgz",
@ -21642,9 +21711,9 @@
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
"caniuse-lite": {
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A=="
"version": "1.0.30001299",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
"integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw=="
},
"capture-exit": {
"version": "2.0.0",
@ -22697,6 +22766,33 @@
"receptacle": "^1.3.2"
}
},
"dns-packet": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.3.1.tgz",
"integrity": "sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==",
"requires": {
"@leichtgewicht/ip-codec": "^2.0.1"
}
},
"dns-query": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/dns-query/-/dns-query-0.8.0.tgz",
"integrity": "sha512-Gx3jYhdj9oLMZFieinpwpTFK0c2Q+teV53Se1+l4AbcWLPMUCBACu7qcj0IqTWwnpasWl8Gwgxeqw2RjoCwIoA==",
"requires": {
"@leichtgewicht/ip-codec": "^2.0.2",
"@types/dns-packet": "^5.2.0",
"dns-packet": "^5.3.0",
"dns-socket": "^4.2.2"
}
},
"dns-socket": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.2.tgz",
"integrity": "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==",
"requires": {
"dns-packet": "^5.2.4"
}
},
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@ -24357,6 +24453,11 @@
}
}
},
"hi-base32": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz",
"integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA=="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",

View File

@ -30,7 +30,7 @@
"pretest": "run-s pretest:*",
"pretest:1-init-git-submodules": "[ -f './nim-waku/build/wakunode2' ] || git submodule update --init --recursive",
"pretest:2-build-nim-waku": "[ -f './nim-waku/build/wakunode2' ] || run-s nim-waku:build",
"examples:pretest": "for d in examples/*; do (cd $d; npm install); done",
"examples:pretest": "for d in examples/*/; do (cd $d && npm install); done",
"nim-waku:build": "(cd nim-waku; NIMFLAGS=\"-d:chronicles_colors=off -d:chronicles_sinks=textlines -d:chronicles_log_level=TRACE\" make -j$(nproc --all 2>/dev/null || echo 2) wakunode2)",
"nim-waku:force-build": "(cd nim-waku && rm -rf ./build/ ./vendor && make -j$(nproc --all 2>/dev/null || echo 2) update) && run-s nim-waku:build",
"test": "run-s build test:*",
@ -39,7 +39,8 @@
"test:spelling": "cspell \"{README.md,.github/*.md,guides/*.md,src/**/*.ts}\"",
"test:unit": "nyc --silent mocha",
"test:karma": "karma start",
"examples:test": "run-s examples:pretest; for d in examples/*; do (cd $d; npm test;); done",
"test:karma-live-data": "LIVE_DATA_TESTS=true karma start ./karma-live-data.conf.js",
"examples:test": "run-s examples:pretest; for d in examples/*/; do (cd $d && npm test;); done",
"proto": "run-s proto:*",
"proto:lint": "buf lint",
"proto:build": "buf generate",
@ -58,7 +59,9 @@
"base64url": "^3.0.1",
"bigint-buffer": "^1.1.5",
"debug": "^4.3.1",
"dns-query": "^0.8.0",
"ecies-geth": "^1.5.2",
"hi-base32": "^0.5.1",
"it-concat": "^2.0.0",
"it-length-prefixed": "^5.0.2",
"js-sha3": "^0.8.0",

View File

@ -1,4 +1,7 @@
export { getBootstrapNodes } from './lib/discovery';
export { getNodesFromHostedJson } from './lib/discovery';
export * as discovery from './lib/discovery';
export * as enr from './lib/enr';
export * as utils from './lib/utils';

View File

@ -0,0 +1,112 @@
import debug from 'debug';
import { Multiaddr } from 'multiaddr';
import { DnsNodeDiscovery } from './dns';
import { getNodesFromHostedJson, getPseudoRandomSubset } from './index';
const dbg = debug('waku:discovery:bootstrap');
/**
* Setup discovery method used to bootstrap.
*
* Only one method is used. `default`, `peers`, `getPeers` and `enr` options are mutually exclusive.
*/
export interface BootstrapOptions {
/**
* The maximum of peers to connect to as part of the bootstrap process.
*
* @default [[Bootstrap.DefaultMaxPeers]]
*/
maxPeers?: number;
/**
* Use the default discovery method. Overrides all other options but `maxPeers`
*
* The default discovery method is likely to change overtime as new discovery
* methods are implemented.
*
* @default false
*/
default?: boolean;
/**
* Multiaddrs of peers to connect to.
*/
peers?: string[];
/**
* Getter that retrieve multiaddrs of peers to connect to.
*/
getPeers?: () => Promise<string[] | Multiaddr[]>;
/**
* An EIP-1459 ENR Tree URL. For example:
* "enrtree://AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C@test.nodes.vac.dev"
*/
enrUrl?: string;
}
/**
* Parse options and expose function to return bootstrap peer addresses.
*/
export class Bootstrap {
public static DefaultMaxPeers = 1;
public readonly getBootstrapPeers: (() => Promise<Multiaddr[]>) | undefined;
constructor(opts: BootstrapOptions) {
const maxPeers = opts.maxPeers ?? Bootstrap.DefaultMaxPeers;
if (opts.default) {
dbg('Bootstrap: Use hosted list of peers.');
this.getBootstrapPeers = getNodesFromHostedJson.bind(
{},
undefined,
undefined,
maxPeers
);
} else if (opts.peers !== undefined && opts.peers.length > 0) {
dbg('Bootstrap: Use provided list of peers.');
const allPeers: Multiaddr[] = opts.peers.map(
(node: string) => new Multiaddr(node)
);
const peers = getPseudoRandomSubset(allPeers, maxPeers);
this.getBootstrapPeers = (): Promise<Multiaddr[]> =>
Promise.resolve(peers);
} else if (typeof opts.getPeers === 'function') {
dbg('Bootstrap: Use provided getPeers function.');
const getPeers = opts.getPeers;
this.getBootstrapPeers = async (): Promise<Multiaddr[]> => {
const allPeers = await getPeers();
return getPseudoRandomSubset<string | Multiaddr>(
allPeers,
maxPeers
).map((node) => new Multiaddr(node));
};
} else if (opts.enrUrl) {
const enrUrl = opts.enrUrl;
dbg('Bootstrap: Use provided EIP-1459 ENR Tree URL.');
const dns = DnsNodeDiscovery.dnsOverHttp();
this.getBootstrapPeers = async (): Promise<Multiaddr[]> => {
const enrs = await dns.getPeers(maxPeers, [enrUrl]);
const addresses: Multiaddr[] = [];
enrs.forEach((enr) => {
if (!enr.multiaddrs) return;
enr.multiaddrs.forEach((ma: Multiaddr) => {
// Only return secure websocket addresses
if (ma.protoNames().includes('wss')) {
addresses.push(ma);
}
});
});
return addresses;
};
} else {
dbg('No bootstrap method specified, no peer will be returned');
this.getBootstrapPeers = undefined;
}
}
}

View File

@ -0,0 +1,203 @@
import { expect } from 'chai';
import { DnsClient, DnsNodeDiscovery } from './dns';
import testData from './testdata.json';
const mockData = testData.dns;
const host = 'nodes.example.org';
const rootDomain = 'JORXBYVVM7AEKETX5DGXW44EAY';
const branchDomainA = 'D2SNLTAGWNQ34NTQTPHNZDECFU';
const branchDomainB = 'D3SNLTAGWNQ34NTQTPHNZDECFU';
const branchDomainC = 'D4SNLTAGWNQ34NTQTPHNZDECFU';
const branchDomainD = 'D5SNLTAGWNQ34NTQTPHNZDECFU';
const partialBranchA = 'AAAA';
const partialBranchB = 'BBBB';
const singleBranch = `enrtree-branch:${branchDomainA}`;
const doubleBranch = `enrtree-branch:${branchDomainA},${branchDomainB}`;
const multiComponentBranch = [
`enrtree-branch:${branchDomainA},${partialBranchA}`,
`${partialBranchB},${branchDomainB}`,
];
// Note: once td.when is asked to throw for an input it will always throw.
// Input can't be re-used for a passing case.
const errorBranchA = `enrtree-branch:${branchDomainC}`;
const errorBranchB = `enrtree-branch:${branchDomainD}`;
/**
* Mocks DNS resolution.
*/
class MockDNS implements DnsClient {
fqdnRes: Map<string, string[]>;
fqdnThrows: string[];
constructor() {
this.fqdnRes = new Map();
this.fqdnThrows = [];
}
addRes(fqdn: string, res: string[]): void {
this.fqdnRes.set(fqdn, res);
}
addThrow(fqdn: string): void {
this.fqdnThrows.push(fqdn);
}
resolveTXT(fqdn: string): Promise<string[]> {
if (this.fqdnThrows.includes(fqdn)) throw 'Mock DNS throws.';
const res = this.fqdnRes.get(fqdn);
if (!res) throw `Mock DNS could not resolve ${fqdn}`;
return Promise.resolve(res);
}
}
describe('DNS Node Discovery', () => {
let mockDns: MockDNS;
beforeEach(() => {
mockDns = new MockDNS();
mockDns.addRes(host, [mockData.enrRoot]);
});
it('retrieves a single peer', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [singleBranch]);
mockDns.addRes(`${branchDomainA}.${host}`, [mockData.enrA]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(1);
expect(peers[0].ip).to.eq('45.77.40.127');
expect(peers[0].tcp).to.eq(30303);
});
it('retrieves all peers (2) when maxQuantity larger than DNS tree size', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [doubleBranch]);
mockDns.addRes(`${branchDomainA}.${host}`, [mockData.enrA]);
mockDns.addRes(`${branchDomainB}.${host}`, [mockData.enrB]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(50, [mockData.enrTree]);
expect(peers.length).to.eq(2);
expect(peers[0].ip).to.not.eq(peers[1].ip);
});
it('retrieves all peers (3) when branch entries are composed of multiple strings', async function () {
mockDns.addRes(`${rootDomain}.${host}`, multiComponentBranch);
mockDns.addRes(`${branchDomainA}.${host}`, [mockData.enr]);
mockDns.addRes(`${branchDomainB}.${host}`, [mockData.enrA]);
mockDns.addRes(`${partialBranchA}${partialBranchB}.${host}`, [
mockData.enrB,
]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(50, [mockData.enrTree]);
expect(peers.length).to.eq(3);
expect(peers[0].ip).to.not.eq(peers[1].ip);
expect(peers[0].ip).to.not.eq(peers[2].ip);
expect(peers[1].ip).to.not.eq(peers[2].ip);
});
it('it tolerates circular branch references', async function () {
// root --> branchA
// branchA --> branchA
mockDns.addRes(`${rootDomain}.${host}`, [singleBranch]);
mockDns.addRes(`${branchDomainA}.${host}`, [singleBranch]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(0);
});
it('recovers when dns.resolve returns empty', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [singleBranch]);
// Empty response case
mockDns.addRes(`${branchDomainA}.${host}`, []);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
let peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(0);
// No TXT records case
mockDns.addRes(`${branchDomainA}.${host}`, []);
peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(0);
});
it('ignores domain fetching errors', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [errorBranchA]);
mockDns.addThrow(`${branchDomainC}.${host}`);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(0);
});
it('ignores unrecognized TXT record formats', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [mockData.enrBranchBadPrefix]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peers = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peers.length).to.eq(0);
});
it('caches peers it previously fetched', async function () {
mockDns.addRes(`${rootDomain}.${host}`, [errorBranchB]);
mockDns.addRes(`${branchDomainD}.${host}`, [mockData.enrA]);
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
const peersA = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peersA.length).to.eq(1);
// Specify that a subsequent network call retrieving the same peer should throw.
// This test passes only if the peer is fetched from cache
mockDns.addThrow(`${branchDomainD}.${host}`);
const peersB = await dnsNodeDiscovery.getPeers(1, [mockData.enrTree]);
expect(peersB.length).to.eq(1);
expect(peersA[0].ip).to.eq(peersB[0].ip);
});
});
describe('DNS Node Discovery [live data]', function () {
const publicKey = 'AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C';
const fqdn = 'test.nodes.vac.dev';
const enrTree = `enrtree://${publicKey}@${fqdn}`;
const ipTestRegex = /^\d+\.\d+\.\d+\.\d+$/;
const maxQuantity = 3;
before(function () {
if (process.env.CI && !process.env.LIVE_DATA_TESTS) {
this.skip();
}
});
it(`should retrieve ${maxQuantity} PeerInfos for test.nodes.vac.dev`, async function () {
this.timeout(5000);
// Google's dns server address. Needs to be set explicitly to run in CI
const dnsNodeDiscovery = DnsNodeDiscovery.dnsOverHttp();
const peers = await dnsNodeDiscovery.getPeers(maxQuantity, [enrTree]);
expect(peers.length).to.eq(maxQuantity);
// TODO: Test multiaddrs entry
console.log(peers.map((peer) => peer.multiaddrs));
const seen: string[] = [];
for (const peer of peers) {
expect(peer!.ip!).to.match(ipTestRegex);
expect(seen).to.not.include(peer!.ip!);
seen.push(peer!.ip!);
}
});
});

201
src/lib/discovery/dns.ts Normal file
View File

@ -0,0 +1,201 @@
import assert from 'assert';
import { debug } from 'debug';
import { ENR } from '../enr';
import { DnsOverHttps, Endpoints } from './dns_over_https';
import { ENRTree } from './enrtree';
const dbg = debug('waku:discovery:dns');
export type SearchContext = {
domain: string;
publicKey: string;
visits: { [key: string]: boolean };
};
export interface DnsClient {
resolveTXT: (domain: string) => Promise<string[]>;
}
export class DnsNodeDiscovery {
private readonly dns: DnsClient;
private readonly _DNSTreeCache: { [key: string]: string };
private readonly _errorTolerance: number = 10;
public static dnsOverHttp(endpoints?: Endpoints): DnsNodeDiscovery {
const dnsClient = new DnsOverHttps(endpoints);
return new DnsNodeDiscovery(dnsClient);
}
/**
* Returns a list of verified peers listed in an EIP-1459 DNS tree. Method may
* return fewer peers than requested if `maxQuantity` is larger than the number
* of ENR records or the number of errors/duplicate peers encountered by randomized
* search exceeds `maxQuantity` plus the `errorTolerance` factor.
*/
async getPeers(maxQuantity: number, enrTreeUrls: string[]): Promise<ENR[]> {
let totalSearches = 0;
const peers: ENR[] = [];
const networkIndex = Math.floor(Math.random() * enrTreeUrls.length);
const { publicKey, domain } = ENRTree.parseTree(enrTreeUrls[networkIndex]);
while (
peers.length < maxQuantity &&
totalSearches < maxQuantity + this._errorTolerance
) {
const context: SearchContext = {
domain,
publicKey,
visits: {},
};
const peer = await this._search(domain, context);
if (peer && isNewPeer(peer, peers)) {
peers.push(peer);
dbg(
`got new peer candidate from DNS address=${peer.nodeId}@${peer.ip}`
);
}
totalSearches++;
}
return peers;
}
public constructor(dns: DnsClient) {
this._DNSTreeCache = {};
this.dns = dns;
}
/**
* Runs a recursive, randomized descent of the DNS tree to retrieve a single
* ENR record as an ENR. Returns null if parsing or DNS resolution fails.
*/
private async _search(
subdomain: string,
context: SearchContext
): Promise<ENR | null> {
const entry = await this._getTXTRecord(subdomain, context);
context.visits[subdomain] = true;
let next: string;
let branches: string[];
const entryType = getEntryType(entry);
try {
switch (entryType) {
case ENRTree.ROOT_PREFIX:
next = ENRTree.parseAndVerifyRoot(entry, context.publicKey);
return await this._search(next, context);
case ENRTree.BRANCH_PREFIX:
branches = ENRTree.parseBranch(entry);
next = selectRandomPath(branches, context);
return await this._search(next, context);
case ENRTree.RECORD_PREFIX:
return ENR.decodeTxt(entry);
default:
return null;
}
} catch (error) {
dbg(
`Failed to search DNS tree ${entryType} at subdomain ${subdomain}: ${error}`
);
return null;
}
}
/**
* Retrieves the TXT record stored at a location from either
* this DNS tree cache or via DNS query
*/
private async _getTXTRecord(
subdomain: string,
context: SearchContext
): Promise<string> {
if (this._DNSTreeCache[subdomain]) {
return this._DNSTreeCache[subdomain];
}
// Location is either the top level tree entry host or a subdomain of it.
const location =
subdomain !== context.domain
? `${subdomain}.${context.domain}`
: context.domain;
const response = await this.dns.resolveTXT(location);
assert(
response.length,
'Received empty result array while fetching TXT record'
);
assert(response[0].length, 'Received empty TXT record');
// Branch entries can be an array of strings of comma delimited subdomains, with
// some subdomain strings split across the array elements
const result = response.join('');
this._DNSTreeCache[subdomain] = result;
return result;
}
}
function getEntryType(entry: string): string {
if (entry.startsWith(ENRTree.ROOT_PREFIX)) return ENRTree.ROOT_PREFIX;
if (entry.startsWith(ENRTree.BRANCH_PREFIX)) return ENRTree.BRANCH_PREFIX;
if (entry.startsWith(ENRTree.RECORD_PREFIX)) return ENRTree.RECORD_PREFIX;
return '';
}
/**
* Returns a randomly selected subdomain string from the list provided by a branch
* entry record.
*
* The client must track subdomains which are already resolved to avoid
* going into an infinite loop b/c branch entries can contain
* circular references. Its in the clients best interest to traverse the
* tree in random order.
*/
function selectRandomPath(branches: string[], context: SearchContext): string {
// Identify domains already visited in this traversal of the DNS tree.
// Then filter against them to prevent cycles.
const circularRefs: { [key: number]: boolean } = {};
for (const [idx, subdomain] of branches.entries()) {
if (context.visits[subdomain]) {
circularRefs[idx] = true;
}
}
// If all possible paths are circular...
if (Object.keys(circularRefs).length === branches.length) {
throw new Error('Unresolvable circular path detected');
}
// Randomly select a viable path
let index;
do {
index = Math.floor(Math.random() * branches.length);
} while (circularRefs[index]);
return branches[index];
}
/**
* @returns false if candidate peer already exists in the
* current collection of peers based on the node id value;
* true otherwise.
*/
function isNewPeer(peer: ENR | null, peers: ENR[]): boolean {
if (!peer || !peer.nodeId) return false;
for (const existingPeer of peers) {
if (peer.nodeId === existingPeer.nodeId) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,61 @@
import { TxtAnswer } from 'dns-packet';
import {
endpoints as defaultEndpoints,
Endpoint,
EndpointProps,
query,
} from 'dns-query';
import { DnsClient } from './dns';
const { cloudflare, google, opendns } = defaultEndpoints;
export type Endpoints =
| 'doh'
| 'dns'
| Iterable<Endpoint | EndpointProps | string>;
export class DnsOverHttps implements DnsClient {
/**
* Create new Dns-Over-Http DNS client.
*
* @param endpoints The endpoints for Dns-Over-Https queries.
* See [dns-query](https://www.npmjs.com/package/dns-query) for details.
* Defaults to cloudflare, google and opendns.
*
* @throws {code: string} If DNS query fails.
*/
public constructor(
public endpoints: Endpoints = [cloudflare, google, opendns]
) {}
async resolveTXT(domain: string): Promise<string[]> {
const response = await query({
questions: [{ type: 'TXT', name: domain }],
});
const answers = response.answers as TxtAnswer[];
const data = answers.map((a) => a.data);
const result: string[] = [];
data.forEach((d) => {
if (typeof d === 'string') {
result.push(d);
} else if (Array.isArray(d)) {
d.forEach((sd) => {
if (typeof sd === 'string') {
result.push(sd);
} else {
result.push(Buffer.from(sd).toString('utf-8'));
}
});
} else {
result.push(Buffer.from(d).toString('utf-8'));
}
});
return result;
}
}

View File

@ -0,0 +1,89 @@
import { expect } from 'chai';
import { ENRTree } from './enrtree';
import testData from './testdata.json';
const dns = testData.dns;
describe('ENRTree', () => {
// Root DNS entries
it('ENRTree (root): should parse and verify and DNS root entry', () => {
const subdomain = ENRTree.parseAndVerifyRoot(dns.enrRoot, dns.publicKey);
expect(subdomain).to.eq('JORXBYVVM7AEKETX5DGXW44EAY');
});
it('ENRTree (root): should error if DNS root entry is mis-prefixed', () => {
try {
ENRTree.parseAndVerifyRoot(dns.enrRootBadPrefix, dns.publicKey);
} catch (e) {
expect(e.toString()).includes(
"ENRTree root entry must start with 'enrtree-root:'"
);
}
});
it('ENRTree (root): should error if DNS root entry signature is invalid', () => {
try {
ENRTree.parseAndVerifyRoot(dns.enrRootBadSig, dns.publicKey);
} catch (e) {
expect(e.toString()).includes('Unable to verify ENRTree root signature');
}
});
it('ENRTree (root): should error if DNS root entry is malformed', () => {
try {
ENRTree.parseAndVerifyRoot(dns.enrRootMalformed, dns.publicKey);
} catch (e) {
expect(e.toString()).includes('Could not parse ENRTree root entry');
}
});
// Tree DNS entries
it('ENRTree (tree): should parse a DNS tree entry', () => {
const { publicKey, domain } = ENRTree.parseTree(dns.enrTree);
expect(publicKey).to.eq(dns.publicKey);
expect(domain).to.eq('nodes.example.org');
});
it('ENRTree (tree): should error if DNS tree entry is mis-prefixed', () => {
try {
ENRTree.parseTree(dns.enrTreeBadPrefix);
} catch (e) {
expect(e.toString()).includes(
"ENRTree tree entry must start with 'enrtree:'"
);
}
});
it('ENRTree (tree): should error if DNS tree entry is misformatted', () => {
try {
ENRTree.parseTree(dns.enrTreeMalformed);
} catch (e) {
expect(e.toString()).includes('Could not parse ENRTree tree entry');
}
});
// Branch entries
it('ENRTree (branch): should parse and verify a single component DNS branch entry', () => {
const expected = [
'D2SNLTAGWNQ34NTQTPHNZDECFU',
'67BLTJEU5R2D5S3B4QKJSBRFCY',
'A2HDMZBB4JIU53VTEGC4TG6P4A',
];
const branches = ENRTree.parseBranch(dns.enrBranch);
expect(branches).to.deep.eq(expected);
});
it('ENRTree (branch): should error if DNS branch entry is mis-prefixed', () => {
try {
ENRTree.parseBranch(dns.enrBranchBadPrefix);
} catch (e) {
expect(e.toString()).includes(
"ENRTree branch entry must start with 'enrtree-branch:'"
);
}
});
});

View File

@ -0,0 +1,116 @@
import assert from 'assert';
import base64url from 'base64url';
import * as base32 from 'hi-base32';
import { ecdsaVerify } from 'secp256k1';
import { ENR } from '../enr';
import { keccak256Buf } from '../utils';
export type ENRRootValues = {
eRoot: string;
lRoot: string;
seq: number;
signature: string;
};
export type ENRTreeValues = {
publicKey: string;
domain: string;
};
export class ENRTree {
public static readonly RECORD_PREFIX = ENR.RECORD_PREFIX;
public static readonly TREE_PREFIX = 'enrtree:';
public static readonly BRANCH_PREFIX = 'enrtree-branch:';
public static readonly ROOT_PREFIX = 'enrtree-root:';
/**
* Extracts the branch subdomain referenced by a DNS tree root string after verifying
* the root record signature with its base32 compressed public key.
*/
static parseAndVerifyRoot(root: string, publicKey: string): string {
assert(
root.startsWith(this.ROOT_PREFIX),
`ENRTree root entry must start with '${this.ROOT_PREFIX}'`
);
const rootValues = ENRTree.parseRootValues(root);
const decodedPublicKey = base32.decode.asBytes(publicKey);
// The signature is a 65-byte secp256k1 over the keccak256 hash
// of the record content, excluding the `sig=` part, encoded as URL-safe base64 string
// (Trailing recovery bit must be trimmed to pass `ecdsaVerify` method)
const signedComponent = root.split(' sig')[0];
const signedComponentBuffer = Buffer.from(signedComponent);
const signatureBuffer = base64url
.toBuffer(rootValues.signature)
.slice(0, 64);
const keyBuffer = Buffer.from(decodedPublicKey);
const isVerified = ecdsaVerify(
signatureBuffer,
keccak256Buf(signedComponentBuffer),
keyBuffer
);
assert(isVerified, 'Unable to verify ENRTree root signature');
return rootValues.eRoot;
}
static parseRootValues(txt: string): ENRRootValues {
const matches = txt.match(
/^enrtree-root:v1 e=([^ ]+) l=([^ ]+) seq=(\d+) sig=([^ ]+)$/
);
assert.ok(Array.isArray(matches), 'Could not parse ENRTree root entry');
matches.shift(); // The first entry is the full match
const [eRoot, lRoot, seq, signature] = matches;
assert.ok(eRoot, "Could not parse 'e' value from ENRTree root entry");
assert.ok(lRoot, "Could not parse 'l' value from ENRTree root entry");
assert.ok(seq, "Could not parse 'seq' value from ENRTree root entry");
assert.ok(signature, "Could not parse 'sig' value from ENRTree root entry");
return { eRoot, lRoot, seq: Number(seq), signature };
}
/**
* Returns the public key and top level domain of an ENR tree entry.
* The domain is the starting point for traversing a set of linked DNS TXT records
* and the public key is used to verify the root entry record
*/
static parseTree(tree: string): ENRTreeValues {
assert(
tree.startsWith(this.TREE_PREFIX),
`ENRTree tree entry must start with '${this.TREE_PREFIX}'`
);
const matches = tree.match(/^enrtree:\/\/([^@]+)@(.+)$/);
assert.ok(Array.isArray(matches), 'Could not parse ENRTree tree entry');
matches.shift(); // The first entry is the full match
const [publicKey, domain] = matches;
assert.ok(publicKey, 'Could not parse public key from ENRTree tree entry');
assert.ok(domain, 'Could not parse domain from ENRTree tree entry');
return { publicKey, domain };
}
/**
* Returns subdomains listed in an ENR branch entry. These in turn lead to
* either further branch entries or ENR records.
*/
static parseBranch(branch: string): string[] {
assert(
branch.startsWith(this.BRANCH_PREFIX),
`ENRTree branch entry must start with '${this.BRANCH_PREFIX}'`
);
return branch.split(this.BRANCH_PREFIX)[1].split(',');
}
}

View File

@ -1,11 +1,3 @@
import axios from 'axios';
import debug from 'debug';
import { shuffle } from 'libp2p-gossipsub/src/utils';
const dbg = debug('waku:discovery');
const DefaultWantedNumber = 1;
/**
* GET list of nodes from remote HTTP host.
*
@ -23,11 +15,20 @@ const DefaultWantedNumber = 1;
* @throws If the remote host is unreachable or the response cannot be parsed
* according to the passed _path_.
*/
export async function getBootstrapNodes(
import axios from 'axios';
import debug from 'debug';
import { Multiaddr } from 'multiaddr';
import { getPseudoRandomSubset } from './index';
const dbg = debug('waku:discovery');
const DefaultWantedNumber = 1;
export async function getNodesFromHostedJson(
path: string[] = ['fleets', 'wakuv2.prod', 'waku-websocket'],
url = 'https://fleets.status.im/',
wantedNumber: number = DefaultWantedNumber
): Promise<string[]> {
): Promise<Multiaddr[]> {
if (wantedNumber <= 0) {
return [];
}
@ -52,15 +53,18 @@ export async function getBootstrapNodes(
}
if (Array.isArray(nodes)) {
return getPseudoRandomSubset(nodes, wantedNumber);
return getPseudoRandomSubset(nodes, wantedNumber).map(
(node: string) => new Multiaddr(node)
);
}
if (typeof nodes === 'string') {
return [nodes];
return [new Multiaddr(nodes)];
}
if (typeof nodes === 'object') {
nodes = Object.values(nodes);
nodes = Object.values(nodes) as string[];
nodes = nodes.map((node: string) => new Multiaddr(node));
return getPseudoRandomSubset(nodes, wantedNumber);
}
@ -68,14 +72,3 @@ export async function getBootstrapNodes(
nodes
)}`;
}
export function getPseudoRandomSubset(
values: string[],
wantedNumber: number
): string[] {
if (values.length <= wantedNumber) {
return values;
}
return shuffle(values).slice(0, wantedNumber);
}

View File

@ -1,6 +1,6 @@
import { expect } from 'chai';
import { getPseudoRandomSubset } from './discovery';
import { getPseudoRandomSubset } from './index';
describe('Discovery', () => {
it('returns all values when wanted number matches available values', function () {

View File

@ -0,0 +1,18 @@
import { shuffle } from 'libp2p-gossipsub/src/utils';
export { getNodesFromHostedJson } from './hosted_json';
export { Bootstrap, BootstrapOptions } from './bootstrap';
export { DnsClient, DnsNodeDiscovery, SearchContext } from './dns';
export { Endpoints, DnsOverHttps } from './dns_over_https';
export { ENRTree, ENRTreeValues, ENRRootValues } from './enrtree';
export function getPseudoRandomSubset<T>(
values: T[],
wantedNumber: number
): T[] {
if (values.length <= wantedNumber) {
return values;
}
return shuffle(values).slice(0, wantedNumber);
}

View File

@ -0,0 +1,18 @@
{
"dns": {
"publicKey": "AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE",
"enr": "enr:-Je4QA1w6JNgH44256YxSTujRYIIy-oeCzL3tIvCIIHEZ_HgWbbFlrtfghWaGKQA9PH2INlnOGiKAU66hhVEoocrZdo0g2V0aMfGhOAp6ZGAgmlkgnY0gmlwhChxb4eJc2VjcDI1NmsxoQMla1-eA4bdHAeDEGv_z115bE16iA4GxcbGd-OlmKnSpYN0Y3CCdl-DdWRwgnZf",
"enrA": "enr:-Jq4QAopXcF_SSfOwl_AmLdrMUnHQO1Rx-XV4gYeySSK32PTbQ8volkh3IQy1ag1Gkl6O-C5rjskj3EyDi8XVzck4PMVg2V0aMrJhKALwySDbxWAgmlkgnY0gmlwhC1NKH-Jc2VjcDI1NmsxoQO5wMEjJLtqT-h6zhef0xsO-SW-pcQD-yuNqCr3GTEZFoN0Y3CCdl-DdWRwgnZf",
"enrB": "enr:-Je4QAFx_6rFjCxCLPUbxIA_KS7FhCYeTU6fXmbj1V08f8DPCUAB9bLoY2Yy7q2hIEby7Yf6e_v7gbofloB1oTnjqeYDg2V0aMfGhOAp6ZGAgmlkgnY0gmlwhLxf-D2Jc2VjcDI1NmsxoQOou7vgUXL96E5CzBsCE6N1GSMqlAACtUxRiNpq6vnB6IN0Y3CCdl-DdWRwgnZf",
"enrRoot": "enrtree-root:v1 e=JORXBYVVM7AEKETX5DGXW44EAY l=FDXN3SN67NA5DKA4J2GOK7BVQI seq=1839 sig=Ma7yIqW2gj59dY8F6plfL7dfotaBPz285mu_XZK1e5VRzNrnf0pCAfacu4fBLuE7jMX-nDbqCM1sFiWWLq8WogE",
"enrBranch": "enrtree-branch:D2SNLTAGWNQ34NTQTPHNZDECFU,67BLTJEU5R2D5S3B4QKJSBRFCY,A2HDMZBB4JIU53VTEGC4TG6P4A",
"enrTree": "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@nodes.example.org",
"enrBadPrefix": "enrabc:-Je4QA1w6JNgH44256YxSTujRYIIy-oeCzL3tIvCIIHEZ_HgWbbFlrtfghWaGKQA9PH2INlnOGiKAU66hhVEoocrZdo0g2V0aMfGhOAp6ZGAgmlkgnY0gmlwhChxb4eJc2VjcDI1NmsxoQMla1-eA4bdHAeDEGv_z115bE16iA4GxcbGd-OlmKnSpYN0Y3CCdl-DdWRwgnZf",
"enrRootBadPrefix": "enrtree:v1 e=JORXBYVVM7AEKETX5DGXW44EAY l=FDXN3SN67NA5DKA4J2GOK7BVQI seq=1839 sig=Ma7yIqW2gj59dY8F6plfL7dfotaBPz285mu_XZK1e5VRzNrnf0pCAfacu4fBLuE7jMX-nDbqCM1sFiWWLq8WogE",
"enrBranchBadPrefix": "Z64M,JOECK7UUYUFVX24QGXYLR3UHDU,RR6SC4GUZBKLFA2WO4IUY6YGEE,EQRME5EAOS7AJHHLDDZNDYT7GI,JXHUMLDSGKU6UQWYFMNCFYQFHQ,4SNDLPNM3CBG2KLBMRSTHWFNP4,WEEEFCKUXOGU4QPKCRBBEHQLEY,CPXM5AOSTICZ3TODJFQACGBWMU,7U26GD37NS6DV72PDAURZI4WUY,MYLQIGMR5GTKPPBMXIINZ2ALGU",
"enrTreeBadPrefix": "entree-branch://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@nodes.example.org",
"enrRootBadSig": "enrtree-root:v1 e=JORXBYVVM7AEKETX5DGXW44EAY l=FDXN3SN67NA5DKA4J2GOK7BVQI seq=1839 sig=Aa7yIqW2gj59dY8F6plfL7dfotaBPz285mu_XZK1e5VRzNrnf0pCAfacu4fBLuE7jMX-nDbqCM1sFiWWLq8WogE",
"enrRootMalformed": "enrtree-root:v1 e=FDXN3SN67NA5DKA4J2GOK7BVQI seq=1839 sig=Ma7yIqW2gj59dY8F6plfL7dfotaBPz285mu_XZK1e5VRzNrnf0pCAfacu4fBLuE7jMX-nDbqCM1sFiWWLq8WogE",
"enrTreeMalformed": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2nodes.example.org"
}
}

View File

@ -113,7 +113,7 @@ describe('ENR', function () {
ENR.decodeTxt(txt);
assert.fail('Expect error here');
} catch (err) {
expect(err.message).to.be.equal('Failed to verify enr: No public key');
expect(err.message).to.be.equal('Failed to verify ENR: No public key');
}
});
});
@ -153,7 +153,7 @@ describe('ENR', function () {
enr.verify(Buffer.alloc(0), Buffer.alloc(0));
assert.fail('Expect error here');
} catch (err) {
expect(err.message).to.be.equal('Failed to verify enr: No public key');
expect(err.message).to.be.equal('Failed to verify ENR: No public key');
}
});

View File

@ -25,6 +25,7 @@ import { ENRKey, ENRValue, NodeId, SequenceNumber } from './types';
import * as v4 from './v4';
export class ENR extends Map<ENRKey, ENRValue> {
public static readonly RECORD_PREFIX = 'enr:';
public seq: SequenceNumber;
public signature: Buffer | null;
@ -93,8 +94,10 @@ export class ENR extends Map<ENRKey, ENRValue> {
}
static decodeTxt(encoded: string): ENR {
if (!encoded.startsWith('enr:')) {
throw new Error("string encoded ENR must start with 'enr:'");
if (!encoded.startsWith(this.RECORD_PREFIX)) {
throw new Error(
`"string encoded ENR must start with '${this.RECORD_PREFIX}'`
);
}
return ENR.decode(base64url.toBuffer(encoded.slice(4)));
}
@ -388,6 +391,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
return new Multiaddr(maBuf);
}
setLocationMultiaddr(multiaddr: Multiaddr): void {
const protoNames = multiaddr.protoNames();
if (
@ -428,7 +432,7 @@ export class ENR extends Map<ENRKey, ENRValue> {
throw new Error(ERR_INVALID_ID);
}
if (!this.publicKey) {
throw new Error('Failed to verify enr: No public key');
throw new Error('Failed to verify ENR: No public key');
}
return v4.verify(this.publicKey, data, signature);
}
@ -471,6 +475,6 @@ export class ENR extends Map<ENRKey, ENRValue> {
}
encodeTxt(privateKey?: Buffer): string {
return 'enr:' + base64url.encode(this.encode(privateKey));
return ENR.RECORD_PREFIX + base64url.encode(this.encode(privateKey));
}
}

View File

@ -4,3 +4,4 @@ export * from './constants';
export * from './enr';
export * from './types';
export * from './create';
export * from './keypair';

View File

@ -1,4 +1,3 @@
import { Buffer } from 'buffer';
import crypto from 'crypto';
import { keccak256 } from 'js-sha3';

View File

@ -1,3 +1,5 @@
import { keccak256, Message } from 'js-sha3';
export function hexToBuf(hex: string | Buffer | Uint8Array): Buffer {
if (typeof hex === 'string') {
return Buffer.from(hex.replace(/^0x/i, ''), 'hex');
@ -31,3 +33,7 @@ export function equalByteArrays(
return aBuf.compare(bBuf) === 0;
}
export function keccak256Buf(message: Message): Buffer {
return Buffer.from(keccak256.arrayBuffer(message));
}

View File

@ -38,7 +38,7 @@ describe('Waku Dial [node only]', function () {
waku = await Waku.create({
staticNoiseKey: NOISE_KEY_1,
bootstrap: true,
bootstrap: { default: true },
});
const connectedPeerID: PeerId = await new Promise((resolve) => {
@ -68,7 +68,7 @@ describe('Waku Dial [node only]', function () {
libp2p: {
modules: { transport: [TCP] },
},
bootstrap: [multiAddrWithId],
bootstrap: { peers: [multiAddrWithId] },
});
const connectedPeerID: PeerId = await new Promise((resolve) => {
@ -102,8 +102,10 @@ describe('Waku Dial [node only]', function () {
libp2p: {
modules: { transport: [TCP] },
},
bootstrap: () => {
return [multiAddrWithId];
bootstrap: {
getPeers: async () => {
return [multiAddrWithId];
},
},
});

View File

@ -2,7 +2,7 @@ import { bytes } from '@chainsafe/libp2p-noise/dist/src/@types/basic';
import { Noise } from '@chainsafe/libp2p-noise/dist/src/noise';
import debug from 'debug';
import Libp2p, { Connection, Libp2pModules, Libp2pOptions } from 'libp2p';
import Bootstrap from 'libp2p-bootstrap';
import Libp2pBootstrap 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
@ -18,7 +18,8 @@ import Ping from 'libp2p/src/ping';
import { Multiaddr, multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
import { getBootstrapNodes } from './discovery';
import { Bootstrap } from './discovery';
import { BootstrapOptions } from './discovery/bootstrap';
import { getPeersForProtocol } from './select_peer';
import { LightPushCodec, WakuLightPush } from './waku_light_push';
import { WakuMessage } from './waku_message';
@ -86,15 +87,12 @@ export interface CreateOptions {
/**
* 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).
* See [[BootstrapOptions]] for available parameters.
*
* Note: It overrides any other peerDiscovery modules that may have been set via
* {@link CreateOptions.libp2p}.
*/
bootstrap?: boolean | string[] | (() => string[] | Promise<string[]>);
bootstrap?: BootstrapOptions;
decryptionKeys?: Array<Uint8Array | string>;
}
@ -189,29 +187,19 @@ export class Waku {
});
if (options?.bootstrap) {
let bootstrap: undefined | (() => string[] | Promise<string[]>);
const bootstrap = new Bootstrap(options?.bootstrap);
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) {
if (bootstrap.getBootstrapPeers !== undefined) {
try {
const list = await bootstrap();
const list = await bootstrap.getBootstrapPeers();
// Note: this overrides any other peer discover
libp2pOpts.modules = Object.assign(libp2pOpts.modules, {
peerDiscovery: [Bootstrap],
peerDiscovery: [Libp2pBootstrap],
});
libp2pOpts.config.peerDiscovery = {
[Bootstrap.tag]: {
[Libp2pBootstrap.tag]: {
list,
enabled: true,
},