mirror of
https://github.com/logos-messaging/logos-messaging-js.git
synced 2026-01-17 15:33:10 +00:00
Split the bootstrap logic in 2 different classes that implement the libp2p peer discovery class. This enables better tree shaking when not using the heaviest version (DNS Discovery). It also means using libp2p interface directly when customizing the peer discovery logic. Finally, the `default` method is still available via the `defaultBootstrap` option.
297 lines
9.1 KiB
TypeScript
297 lines
9.1 KiB
TypeScript
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.enrWithWaku2Relay]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(1);
|
|
expect(peers[0].ip).to.eq("192.168.178.251");
|
|
expect(peers[0].tcp).to.eq(8002);
|
|
});
|
|
|
|
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([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
|
|
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([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(0);
|
|
|
|
// No TXT records case
|
|
mockDns.addRes(`${branchDomainA}.${host}`, []);
|
|
|
|
peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], { relay: 1 });
|
|
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([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
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([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
expect(peers.length).to.eq(0);
|
|
});
|
|
|
|
it("caches peers it previously fetched", async function () {
|
|
mockDns.addRes(`${rootDomain}.${host}`, [errorBranchB]);
|
|
mockDns.addRes(`${branchDomainD}.${host}`, [mockData.enrWithWaku2Relay]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peersA = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
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([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
expect(peersB.length).to.eq(1);
|
|
expect(peersA[0].ip).to.eq(peersB[0].ip);
|
|
});
|
|
});
|
|
|
|
describe("DNS Node Discovery w/ capabilities", () => {
|
|
let mockDns: MockDNS;
|
|
|
|
beforeEach(() => {
|
|
mockDns = new MockDNS();
|
|
mockDns.addRes(host, [mockData.enrRoot]);
|
|
});
|
|
|
|
it("should only return 1 node with relay capability", async () => {
|
|
mockDns.addRes(`${rootDomain}.${host}`, [mockData.enrWithWaku2Relay]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
relay: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(1);
|
|
expect(peers[0].peerId?.toString()).to.eq(
|
|
"16Uiu2HAmPsYLvfKafxgRsb6tioYyGnSvGXS2iuMigptHrqHPNPzx"
|
|
);
|
|
});
|
|
|
|
it("should only return 1 node with relay and store capability", async () => {
|
|
mockDns.addRes(`${rootDomain}.${host}`, [mockData.enrWithWaku2RelayStore]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
store: 1,
|
|
relay: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(1);
|
|
expect(peers[0].peerId?.toString()).to.eq(
|
|
"16Uiu2HAm2HyS6brcCspSbszG9i36re2bWBVjMe3tMdnFp1Hua34F"
|
|
);
|
|
});
|
|
|
|
it("should only return 1 node with store capability", async () => {
|
|
mockDns.addRes(`${rootDomain}.${host}`, [mockData.enrWithWaku2Store]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
store: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(1);
|
|
expect(peers[0].peerId?.toString()).to.eq(
|
|
"16Uiu2HAkv3La3ECgQpdYeEJfrX36EWdhkUDv4C9wvXM8TFZ9dNgd"
|
|
);
|
|
});
|
|
|
|
it("retrieves all peers (2) when cannot fulfill all requirements", async () => {
|
|
mockDns.addRes(`${rootDomain}.${host}`, [doubleBranch]);
|
|
mockDns.addRes(`${branchDomainA}.${host}`, [
|
|
mockData.enrWithWaku2RelayStore,
|
|
]);
|
|
mockDns.addRes(`${branchDomainB}.${host}`, [mockData.enrWithWaku2Relay]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
store: 1,
|
|
relay: 2,
|
|
filter: 1,
|
|
});
|
|
|
|
expect(peers.length).to.eq(2);
|
|
const peerIds = peers.map((p) => p.peerId?.toString());
|
|
expect(peerIds).to.contain(
|
|
"16Uiu2HAm2HyS6brcCspSbszG9i36re2bWBVjMe3tMdnFp1Hua34F"
|
|
);
|
|
expect(peerIds).to.contain(
|
|
"16Uiu2HAmPsYLvfKafxgRsb6tioYyGnSvGXS2iuMigptHrqHPNPzx"
|
|
);
|
|
});
|
|
|
|
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.enrWithWaku2RelayStore,
|
|
]);
|
|
mockDns.addRes(`${branchDomainB}.${host}`, [mockData.enrWithWaku2Relay]);
|
|
mockDns.addRes(`${partialBranchA}${partialBranchB}.${host}`, [
|
|
mockData.enrWithWaku2Store,
|
|
]);
|
|
|
|
const dnsNodeDiscovery = new DnsNodeDiscovery(mockDns);
|
|
const peers = await dnsNodeDiscovery.getPeers([mockData.enrTree], {
|
|
store: 2,
|
|
relay: 2,
|
|
});
|
|
|
|
expect(peers.length).to.eq(3);
|
|
const peerIds = peers.map((p) => p.peerId?.toString());
|
|
expect(peerIds).to.contain(
|
|
"16Uiu2HAm2HyS6brcCspSbszG9i36re2bWBVjMe3tMdnFp1Hua34F"
|
|
);
|
|
expect(peerIds).to.contain(
|
|
"16Uiu2HAmPsYLvfKafxgRsb6tioYyGnSvGXS2iuMigptHrqHPNPzx"
|
|
);
|
|
expect(peerIds).to.contain(
|
|
"16Uiu2HAkv3La3ECgQpdYeEJfrX36EWdhkUDv4C9wvXM8TFZ9dNgd"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("DNS Node Discovery [live data]", function () {
|
|
const publicKey = "AOFTICU2XWDULNLZGRMQS4RIZPAZEHYMV4FYHAPW563HNRAOERP7C";
|
|
const fqdn = "test.waku.nodes.status.im";
|
|
const enrTree = `enrtree://${publicKey}@${fqdn}`;
|
|
const maxQuantity = 3;
|
|
|
|
before(function () {
|
|
if (process.env.CI) {
|
|
this.skip();
|
|
}
|
|
});
|
|
|
|
it(`should retrieve ${maxQuantity} multiaddrs for test.waku.nodes.status.im`, async function () {
|
|
this.timeout(10000);
|
|
// Google's dns server address. Needs to be set explicitly to run in CI
|
|
const dnsNodeDiscovery = DnsNodeDiscovery.dnsOverHttp();
|
|
const peers = await dnsNodeDiscovery.getPeers([enrTree], {
|
|
relay: maxQuantity,
|
|
store: maxQuantity,
|
|
filter: maxQuantity,
|
|
lightPush: maxQuantity,
|
|
});
|
|
|
|
expect(peers.length).to.eq(maxQuantity);
|
|
|
|
const multiaddrs = peers.map((peer) => peer.multiaddrs).flat();
|
|
|
|
const seen: string[] = [];
|
|
for (const ma of multiaddrs) {
|
|
expect(ma).to.not.be.undefined;
|
|
expect(seen).to.not.include(ma!.toString());
|
|
seen.push(ma!.toString());
|
|
}
|
|
});
|
|
});
|