diff --git a/package-lock.json b/package-lock.json index 042dd99ee1..043556cef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28228,6 +28228,7 @@ }, "devDependencies": { "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/peer-id-factory": "^2.0.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", @@ -28274,6 +28275,7 @@ "@libp2p/interface-connection-manager": "^1.3.7", "@libp2p/interface-libp2p": "^1.1.1", "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", "@libp2p/interface-registrar": "^2.0.8", "@multiformats/multiaddr": "^11.4.0", @@ -28520,16 +28522,13 @@ "version": "0.0.1", "license": "MIT OR Apache-2.0", "dependencies": { - "@libp2p/peer-id": "^2.0.2", "debug": "^4.3.4", "uint8arrays": "^4.0.3" }, "devDependencies": { "@libp2p/interface-connection": "^3.0.8", "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", - "@multiformats/multiaddr": "^11.4.0", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", @@ -32902,6 +32901,7 @@ "@ethersproject/rlp": "^5.7.0", "@libp2p/crypto": "^1.0.12", "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/peer-id": "^2.0.2", "@libp2p/peer-id-factory": "^2.0.1", "@multiformats/multiaddr": "^11.4.0", @@ -32949,6 +32949,7 @@ "@libp2p/interface-connection-manager": "^1.3.7", "@libp2p/interface-libp2p": "^1.1.1", "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", "@libp2p/interface-registrar": "^2.0.8", "@multiformats/multiaddr": "^11.4.0", @@ -33151,10 +33152,7 @@ "requires": { "@libp2p/interface-connection": "^3.0.8", "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", - "@libp2p/peer-id": "^2.0.2", - "@multiformats/multiaddr": "^11.4.0", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/dns-discovery/src/dns.ts b/packages/dns-discovery/src/dns.ts index 6052be6df8..41693b3204 100644 --- a/packages/dns-discovery/src/dns.ts +++ b/packages/dns-discovery/src/dns.ts @@ -1,4 +1,4 @@ -import { ENR } from "@waku/enr"; +import { ENR, EnrDecoder } from "@waku/enr"; import type { IEnr } from "@waku/interfaces"; import debug from "debug"; @@ -131,7 +131,7 @@ export class DnsNodeDiscovery { next = selectRandomPath(branches, context); return await this._search(next, context); case ENRTree.RECORD_PREFIX: - return ENR.decodeTxt(entry); + return EnrDecoder.fromString(entry); default: return null; } diff --git a/packages/dns-discovery/src/fetch_nodes.spec.ts b/packages/dns-discovery/src/fetch_nodes.spec.ts index 93eaffbbd0..efb085c7aa 100644 --- a/packages/dns-discovery/src/fetch_nodes.spec.ts +++ b/packages/dns-discovery/src/fetch_nodes.spec.ts @@ -1,6 +1,7 @@ import { createSecp256k1PeerId } from "@libp2p/peer-id-factory"; import { multiaddr } from "@multiformats/multiaddr"; import { ENR } from "@waku/enr"; +import { EnrCreator } from "@waku/enr"; import type { Waku2 } from "@waku/interfaces"; import { expect } from "chai"; @@ -8,7 +9,7 @@ import { fetchNodesUntilCapabilitiesFulfilled } from "./fetch_nodes.js"; async function createEnr(waku2: Waku2): Promise { const peerId = await createSecp256k1PeerId(); - const enr = await ENR.createFromPeerId(peerId); + const enr = await EnrCreator.fromPeerId(peerId); enr.setLocationMultiaddr(multiaddr("/ip4/18.223.219.100/udp/9000")); enr.multiaddrs = [ multiaddr("/dns4/node1.do-ams.wakuv2.test.statusim.net/tcp/443/wss"), diff --git a/packages/dns-discovery/src/index.ts b/packages/dns-discovery/src/index.ts index d768ff2647..0151714c68 100644 --- a/packages/dns-discovery/src/index.ts +++ b/packages/dns-discovery/src/index.ts @@ -7,7 +7,6 @@ import type { PeerInfo } from "@libp2p/interface-peer-info"; import type { PeerStore } from "@libp2p/interface-peer-store"; import { CustomEvent, EventEmitter } from "@libp2p/interfaces/events"; import type { IEnr } from "@waku/interfaces"; -import { multiaddrsToPeerInfo } from "@waku/utils"; import debug from "debug"; import { DnsNodeDiscovery, NodeCapabilityCount } from "./dns.js"; @@ -94,27 +93,28 @@ export class PeerDiscoveryDns this._started = true; for await (const peer of this.nextPeer()) { if (!this._started) return; - const peerInfos = multiaddrsToPeerInfo(peer.getFullMultiaddrs()); - peerInfos.forEach(async (peerInfo) => { - if ( - (await this._components.peerStore.getTags(peerInfo.id)).find( - ({ name }) => name === DEFAULT_BOOTSTRAP_TAG_NAME - ) - ) - return; - await this._components.peerStore.tagPeer( - peerInfo.id, - DEFAULT_BOOTSTRAP_TAG_NAME, - { - value: this._options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE, - ttl: this._options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL, - } - ); - this.dispatchEvent( - new CustomEvent("peer", { detail: peerInfo }) - ); - }); + const peerInfo = peer.peerInfo; + if (!peerInfo) continue; + + if ( + (await this._components.peerStore.getTags(peerInfo.id)).find( + ({ name }) => name === DEFAULT_BOOTSTRAP_TAG_NAME + ) + ) + continue; + + await this._components.peerStore.tagPeer( + peerInfo.id, + DEFAULT_BOOTSTRAP_TAG_NAME, + { + value: this._options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE, + ttl: this._options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL, + } + ); + this.dispatchEvent( + new CustomEvent("peer", { detail: peerInfo }) + ); } } diff --git a/packages/enr/package.json b/packages/enr/package.json index fcd81666e0..c8306c4c9e 100644 --- a/packages/enr/package.json +++ b/packages/enr/package.json @@ -64,6 +64,7 @@ }, "devDependencies": { "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/peer-id-factory": "^2.0.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", diff --git a/packages/enr/src/creator.ts b/packages/enr/src/creator.ts new file mode 100644 index 0000000000..b310789184 --- /dev/null +++ b/packages/enr/src/creator.ts @@ -0,0 +1,36 @@ +import { PeerId } from "@libp2p/interface-peer-id"; +import type { ENRKey, ENRValue } from "@waku/interfaces"; +import { utf8ToBytes } from "@waku/utils"; + +import { compressPublicKey } from "./crypto.js"; +import { ENR } from "./enr.js"; +import { getPublicKeyFromPeerId } from "./peer_id.js"; + +export class EnrCreator { + static fromPublicKey( + publicKey: Uint8Array, + kvs: Record = {} + ): Promise { + // EIP-778 specifies that the key must be in compressed format, 33 bytes + if (publicKey.length !== 33) { + publicKey = compressPublicKey(publicKey); + } + return ENR.create({ + ...kvs, + id: utf8ToBytes("v4"), + secp256k1: publicKey, + }); + } + + static async fromPeerId( + peerId: PeerId, + kvs: Record = {} + ): Promise { + switch (peerId.type) { + case "secp256k1": + return EnrCreator.fromPublicKey(getPublicKeyFromPeerId(peerId), kvs); + default: + throw new Error(); + } + } +} diff --git a/packages/enr/src/crypto.ts b/packages/enr/src/crypto.ts index a265710a2c..4d9f8729b9 100644 --- a/packages/enr/src/crypto.ts +++ b/packages/enr/src/crypto.ts @@ -2,14 +2,6 @@ import * as secp from "@noble/secp256k1"; import { concat } from "@waku/utils"; import sha3 from "js-sha3"; -export const randomBytes = secp.utils.randomBytes; - -/** - * Return the public key for the given private key, to be used for asymmetric - * encryption. - */ -export const getPublicKey = secp.getPublicKey; - /** * ECDSA Sign a message with the given private key. * diff --git a/packages/enr/src/decoder.ts b/packages/enr/src/decoder.ts new file mode 100644 index 0000000000..12f1dfc241 --- /dev/null +++ b/packages/enr/src/decoder.ts @@ -0,0 +1,84 @@ +import * as RLP from "@ethersproject/rlp"; +import type { ENRKey, ENRValue } from "@waku/interfaces"; +import { bytesToHex, bytesToUtf8, hexToBytes } from "@waku/utils"; +import { log } from "debug"; +import { fromString } from "uint8arrays/from-string"; + +import { ENR } from "./enr.js"; + +export class EnrDecoder { + static fromString(encoded: string): Promise { + if (!encoded.startsWith(ENR.RECORD_PREFIX)) { + throw new Error( + `"string encoded ENR must start with '${ENR.RECORD_PREFIX}'` + ); + } + return EnrDecoder.fromRLP(fromString(encoded.slice(4), "base64url")); + } + + static fromRLP(encoded: Uint8Array): Promise { + const decoded = RLP.decode(encoded).map(hexToBytes); + return fromValues(decoded); + } +} + +async function fromValues(values: Uint8Array[]): Promise { + const { signature, seq, kvs } = checkValues(values); + + const obj: Record = {}; + for (let i = 0; i < kvs.length; i += 2) { + try { + obj[bytesToUtf8(kvs[i])] = kvs[i + 1]; + } catch (e) { + log("Failed to decode ENR key to UTF-8, skipping it", kvs[i], e); + } + } + const _seq = decodeSeq(seq); + + const enr = await ENR.create(obj, _seq, signature); + checkSignature(seq, kvs, enr, signature); + return enr; +} + +function decodeSeq(seq: Uint8Array): bigint { + // If seq is an empty array, translate as value 0 + if (!seq.length) return BigInt(0); + + return BigInt("0x" + bytesToHex(seq)); +} + +function checkValues(values: Uint8Array[]): { + signature: Uint8Array; + seq: Uint8Array; + kvs: Uint8Array[]; +} { + if (!Array.isArray(values)) { + throw new Error("Decoded ENR must be an array"); + } + if (values.length % 2 !== 0) { + throw new Error("Decoded ENR must have an even number of elements"); + } + const [signature, seq, ...kvs] = values; + if (!signature || Array.isArray(signature)) { + throw new Error("Decoded ENR invalid signature: must be a byte array"); + } + if (!seq || Array.isArray(seq)) { + throw new Error( + "Decoded ENR invalid sequence number: must be a byte array" + ); + } + + return { signature, seq, kvs }; +} + +function checkSignature( + seq: Uint8Array, + kvs: Uint8Array[], + enr: ENR, + signature: Uint8Array +): void { + const rlpEncodedBytes = hexToBytes(RLP.encode([seq, ...kvs])); + if (!enr.verify(rlpEncodedBytes, signature)) { + throw new Error("Unable to verify ENR signature"); + } +} diff --git a/packages/enr/src/encoder.ts b/packages/enr/src/encoder.ts new file mode 100644 index 0000000000..9076ab8fdf --- /dev/null +++ b/packages/enr/src/encoder.ts @@ -0,0 +1,50 @@ +import * as RLP from "@ethersproject/rlp"; +import type { ENRKey, ENRValue } from "@waku/interfaces"; +import { hexToBytes, utf8ToBytes } from "@waku/utils"; +import { toString } from "uint8arrays/to-string"; + +import { ERR_NO_SIGNATURE, MAX_RECORD_SIZE } from "./constants.js"; +import { ENR } from "./enr.js"; + +export class EnrEncoder { + static async toValues( + enr: ENR, + privateKey?: Uint8Array + ): Promise<(ENRKey | ENRValue | number[])[]> { + // sort keys and flatten into [k, v, k, v, ...] + const content: Array = Array.from(enr.keys()) + .sort((a, b) => a.localeCompare(b)) + .map((k) => [k, enr.get(k)] as [ENRKey, ENRValue]) + .map(([k, v]) => [utf8ToBytes(k), v]) + .flat(); + content.unshift(new Uint8Array([Number(enr.seq)])); + if (privateKey) { + content.unshift( + await enr.sign(hexToBytes(RLP.encode(content)), privateKey) + ); + } else { + if (!enr.signature) { + throw new Error(ERR_NO_SIGNATURE); + } + content.unshift(enr.signature); + } + return content; + } + + static async toBytes(enr: ENR, privateKey?: Uint8Array): Promise { + const encoded = hexToBytes( + RLP.encode(await EnrEncoder.toValues(enr, privateKey)) + ); + if (encoded.length >= MAX_RECORD_SIZE) { + throw new Error("ENR must be less than 300 bytes"); + } + return encoded; + } + + static async toString(enr: ENR, privateKey?: Uint8Array): Promise { + return ( + ENR.RECORD_PREFIX + + toString(await EnrEncoder.toBytes(enr, privateKey), "base64url") + ); + } +} diff --git a/packages/enr/src/enr.spec.ts b/packages/enr/src/enr.spec.ts index c45a1eb2b2..f498089f4d 100644 --- a/packages/enr/src/enr.spec.ts +++ b/packages/enr/src/enr.spec.ts @@ -1,21 +1,29 @@ +import type { PeerId } from "@libp2p/interface-peer-id"; import { createSecp256k1PeerId } from "@libp2p/peer-id-factory"; import { multiaddr } from "@multiformats/multiaddr"; +import * as secp from "@noble/secp256k1"; import type { Waku2 } from "@waku/interfaces"; import { bytesToHex, hexToBytes, utf8ToBytes } from "@waku/utils"; import { assert, expect } from "chai"; import { equals } from "uint8arrays/equals"; import { ERR_INVALID_ID } from "./constants.js"; -import { getPublicKey } from "./crypto.js"; -import { ENR } from "./enr.js"; -import { createKeypairFromPeerId, IKeypair } from "./keypair/index.js"; +import { EnrCreator } from "./creator.js"; +import { EnrDecoder } from "./decoder.js"; +import { EnrEncoder } from "./encoder.js"; +import { + ENR, + TransportProtocol, + TransportProtocolPerIpVersion, +} from "./enr.js"; +import { getPrivateKeyFromPeerId } from "./peer_id.js"; describe("ENR", function () { describe("Txt codec", () => { it("should encodeTxt and decodeTxt", async () => { const peerId = await createSecp256k1PeerId(); - const enr = await ENR.createFromPeerId(peerId); - const keypair = await createKeypairFromPeerId(peerId); + const enr = await EnrCreator.fromPeerId(peerId); + const privateKey = await getPrivateKeyFromPeerId(peerId); enr.setLocationMultiaddr(multiaddr("/ip4/18.223.219.100/udp/9000")); enr.multiaddrs = [ multiaddr("/dns4/node1.do-ams.wakuv2.test.statusim.net/tcp/443/wss"), @@ -32,14 +40,14 @@ describe("ENR", function () { lightPush: false, }; - const txt = await enr.encodeTxt(keypair.privateKey); - const enr2 = await ENR.decodeTxt(txt); + const txt = await EnrEncoder.toString(enr, privateKey); + const enr2 = await EnrDecoder.fromString(txt); if (!enr.signature) throw "enr.signature is undefined"; if (!enr2.signature) throw "enr.signature is undefined"; expect(bytesToHex(enr2.signature)).to.be.equal(bytesToHex(enr.signature)); - const ma = enr2.getLocationMultiaddr("udp")!; + const ma = enr2.getLocationMultiaddr(TransportProtocol.UDP)!; expect(ma.toString()).to.be.equal("/ip4/18.223.219.100/udp/9000"); expect(enr2.multiaddrs).to.not.be.undefined; expect(enr2.multiaddrs!.length).to.be.equal(3); @@ -64,7 +72,7 @@ describe("ENR", function () { it("should decode valid enr successfully", async () => { const txt = "enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg"; - const enr = await ENR.decodeTxt(txt); + const enr = await EnrDecoder.fromString(txt); const eth2 = enr.get("eth2"); if (!eth2) throw "eth2 is undefined"; expect(bytesToHex(eth2)).to.be.equal("f6775d0700000113ffffffffffff1f00"); @@ -73,7 +81,7 @@ describe("ENR", function () { it("should decode valid ENR with multiaddrs successfully [shared test vector]", async () => { const txt = "enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSHKCyJDGRLBGsloNbS8AZF33IVuefjOO6BIJpZIJ2NIJpcIQS39tkim11bHRpYWRkcnO4lgAvNihub2RlLTAxLmRvLWFtczMud2FrdXYyLnRlc3Quc3RhdHVzaW0ubmV0BgG73gMAODcxbm9kZS0wMS5hYy1jbi1ob25na29uZy1jLndha3V2Mi50ZXN0LnN0YXR1c2ltLm5ldAYBu94DACm9A62t7AQL4Ef5ZYZosRpQTzFVAB8jGjf1TER2wH-0zBOe1-MDBNLeA4lzZWNwMjU2azGhAzfsxbxyCkgCqq8WwYsVWH7YkpMLnU2Bw5xJSimxKav-g3VkcIIjKA"; - const enr = await ENR.decodeTxt(txt); + const enr = await EnrDecoder.fromString(txt); expect(enr.multiaddrs).to.not.be.undefined; expect(enr.multiaddrs!.length).to.be.equal(3); @@ -92,7 +100,7 @@ describe("ENR", function () { it("should decode valid enr with tcp successfully", async () => { const txt = "enr:-IS4QAmC_o1PMi5DbR4Bh4oHVyQunZblg4bTaottPtBodAhJZvxVlWW-4rXITPNg4mwJ8cW__D9FBDc9N4mdhyMqB-EBgmlkgnY0gmlwhIbRi9KJc2VjcDI1NmsxoQOevTdO6jvv3fRruxguKR-3Ge4bcFsLeAIWEDjrfaigNoN0Y3CCdl8"; - const enr = await ENR.decodeTxt(txt); + const enr = await EnrDecoder.fromString(txt); expect(enr.tcp).to.not.be.undefined; expect(enr.tcp).to.be.equal(30303); expect(enr.ip).to.not.be.undefined; @@ -106,14 +114,14 @@ describe("ENR", function () { it("should throw error - no id", async () => { try { const peerId = await createSecp256k1PeerId(); - const enr = await ENR.createFromPeerId(peerId); - const keypair = await createKeypairFromPeerId(peerId); + const enr = await EnrCreator.fromPeerId(peerId); + const privateKey = await getPrivateKeyFromPeerId(peerId); enr.setLocationMultiaddr(multiaddr("/ip4/18.223.219.100/udp/9000")); enr.set("id", new Uint8Array([0])); - const txt = await enr.encodeTxt(keypair.privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); - await ENR.decodeTxt(txt); + await EnrDecoder.fromString(txt); assert.fail("Expect error here"); } catch (err: unknown) { const e = err as Error; @@ -125,7 +133,7 @@ describe("ENR", function () { try { const txt = "enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2d11eu6dCsxoQIB_c-jQMOXsbjWkbN-kj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk"; - ENR.decodeTxt(txt); + EnrDecoder.fromString(txt); assert.fail("Expect error here"); } catch (err: unknown) { const e = err as Error; @@ -179,7 +187,7 @@ describe("ENR", function () { it("should return false", async () => { const txt = "enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg"; - const enr = await ENR.decodeTxt(txt); + const enr = await EnrDecoder.fromString(txt); // should have id and public key inside ENR expect(enr.verify(new Uint8Array(32), new Uint8Array(64))).to.be.false; }); @@ -194,10 +202,10 @@ describe("ENR", function () { privateKey = hexToBytes( "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" ); - record = await ENR.createV4(getPublicKey(privateKey)); + record = await EnrCreator.fromPublicKey(secp.getPublicKey(privateKey)); record.setLocationMultiaddr(multiaddr("/ip4/127.0.0.1/udp/30303")); record.seq = seq; - await record.encodeTxt(privateKey); + await EnrEncoder.toString(record, privateKey); }); it("should properly compute the node id", () => { @@ -207,8 +215,8 @@ describe("ENR", function () { }); it("should encode/decode to RLP encoding", async function () { - const encoded = await record.encode(privateKey); - const decoded = await ENR.decode(encoded); + const encoded = await EnrEncoder.toBytes(record, privateKey); + const decoded = await EnrDecoder.fromRLP(encoded); record.forEach((value, key) => { expect(equals(decoded.get(key)!, value)).to.be.true; @@ -219,7 +227,7 @@ describe("ENR", function () { // spec enr https://eips.ethereum.org/EIPS/eip-778 const testTxt = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; - const decoded = await ENR.decodeTxt(testTxt); + const decoded = await EnrDecoder.fromString(testTxt); // Note: Signatures are different due to the extra entropy added // by @noble/secp256k1: // https://github.com/paulmillr/noble-secp256k1#signmsghash-privatekey @@ -239,7 +247,7 @@ describe("ENR", function () { privateKey = hexToBytes( "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" ); - record = await ENR.createV4(getPublicKey(privateKey)); + record = await EnrCreator.fromPublicKey(secp.getPublicKey(privateKey)); }); it("should get / set UDP multiaddr", () => { @@ -253,16 +261,16 @@ describe("ENR", function () { record.set("ip", tuples0[0][1]); record.set("udp", tuples0[1][1]); // and get the multiaddr - expect(record.getLocationMultiaddr("udp")!.toString()).to.equal( - multi0.toString() - ); + expect( + record.getLocationMultiaddr(TransportProtocol.UDP)!.toString() + ).to.equal(multi0.toString()); // set the multiaddr const multi1 = multiaddr("/ip4/0.0.0.0/udp/30300"); record.setLocationMultiaddr(multi1); // and get the multiaddr - expect(record.getLocationMultiaddr("udp")!.toString()).to.equal( - multi1.toString() - ); + expect( + record.getLocationMultiaddr(TransportProtocol.UDP)!.toString() + ).to.equal(multi1.toString()); // and get the underlying records const tuples1 = multi1.tuples(); expect(record.get("ip")).to.deep.equal(tuples1[0][1]); @@ -281,16 +289,16 @@ describe("ENR", function () { record.set("ip", tuples0[0][1]); record.set("tcp", tuples0[1][1]); // and get the multiaddr - expect(record.getLocationMultiaddr("tcp")!.toString()).to.equal( - multi0.toString() - ); + expect( + record.getLocationMultiaddr(TransportProtocol.TCP)!.toString() + ).to.equal(multi0.toString()); // set the multiaddr const multi1 = multiaddr("/ip4/0.0.0.0/tcp/30300"); record.setLocationMultiaddr(multi1); // and get the multiaddr - expect(record.getLocationMultiaddr("tcp")!.toString()).to.equal( - multi1.toString() - ); + expect( + record.getLocationMultiaddr(TransportProtocol.TCP)!.toString() + ).to.equal(multi1.toString()); // and get the underlying records const tuples1 = multi1.tuples(); expect(record.get("ip")).to.deep.equal(tuples1[0][1]); @@ -303,12 +311,12 @@ describe("ENR", function () { const ip6 = "::1"; const tcp = 8080; const udp = 8080; - let peerId; + let peerId: PeerId; let enr: ENR; before(async function () { peerId = await createSecp256k1PeerId(); - enr = await ENR.createFromPeerId(peerId); + enr = await EnrCreator.fromPeerId(peerId); enr.ip = ip4; enr.ip6 = ip6; enr.tcp = tcp; @@ -318,43 +326,43 @@ describe("ENR", function () { }); it("should properly create location multiaddrs - udp4", () => { - expect(enr.getLocationMultiaddr("udp4")).to.deep.equal( - multiaddr(`/ip4/${ip4}/udp/${udp}`) - ); + expect( + enr.getLocationMultiaddr(TransportProtocolPerIpVersion.UDP4) + ).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${udp}`)); }); it("should properly create location multiaddrs - tcp4", () => { - expect(enr.getLocationMultiaddr("tcp4")).to.deep.equal( - multiaddr(`/ip4/${ip4}/tcp/${tcp}`) - ); + expect( + enr.getLocationMultiaddr(TransportProtocolPerIpVersion.TCP4) + ).to.deep.equal(multiaddr(`/ip4/${ip4}/tcp/${tcp}`)); }); it("should properly create location multiaddrs - udp6", () => { - expect(enr.getLocationMultiaddr("udp6")).to.deep.equal( - multiaddr(`/ip6/${ip6}/udp/${udp}`) - ); + expect( + enr.getLocationMultiaddr(TransportProtocolPerIpVersion.UDP6) + ).to.deep.equal(multiaddr(`/ip6/${ip6}/udp/${udp}`)); }); it("should properly create location multiaddrs - tcp6", () => { - expect(enr.getLocationMultiaddr("tcp6")).to.deep.equal( - multiaddr(`/ip6/${ip6}/tcp/${tcp}`) - ); + expect( + enr.getLocationMultiaddr(TransportProtocolPerIpVersion.TCP6) + ).to.deep.equal(multiaddr(`/ip6/${ip6}/tcp/${tcp}`)); }); it("should properly create location multiaddrs - udp", () => { // default to ip4 - expect(enr.getLocationMultiaddr("udp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.UDP)).to.deep.equal( multiaddr(`/ip4/${ip4}/udp/${udp}`) ); // if ip6 is set, use it enr.ip = undefined; - expect(enr.getLocationMultiaddr("udp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.UDP)).to.deep.equal( multiaddr(`/ip6/${ip6}/udp/${udp}`) ); // if ip6 does not exist, use ip4 enr.ip6 = undefined; enr.ip = ip4; - expect(enr.getLocationMultiaddr("udp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.UDP)).to.deep.equal( multiaddr(`/ip4/${ip4}/udp/${udp}`) ); enr.ip6 = ip6; @@ -362,34 +370,53 @@ describe("ENR", function () { it("should properly create location multiaddrs - tcp", () => { // default to ip4 - expect(enr.getLocationMultiaddr("tcp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.TCP)).to.deep.equal( multiaddr(`/ip4/${ip4}/tcp/${tcp}`) ); // if ip6 is set, use it enr.ip = undefined; - expect(enr.getLocationMultiaddr("tcp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.TCP)).to.deep.equal( multiaddr(`/ip6/${ip6}/tcp/${tcp}`) ); // if ip6 does not exist, use ip4 enr.ip6 = undefined; enr.ip = ip4; - expect(enr.getLocationMultiaddr("tcp")).to.deep.equal( + expect(enr.getLocationMultiaddr(TransportProtocol.TCP)).to.deep.equal( multiaddr(`/ip4/${ip4}/tcp/${tcp}`) ); enr.ip6 = ip6; }); + + it("should properly create peer info with all multiaddrs", () => { + const peerInfo = enr.peerInfo!; + console.log(peerInfo); + expect(peerInfo.id.toString()).to.equal(peerId.toString()); + expect(peerInfo.multiaddrs.length).to.equal(4); + expect(peerInfo.multiaddrs.map((ma) => ma.toString())).to.contain( + multiaddr(`/ip4/${ip4}/tcp/${tcp}`).toString() + ); + expect(peerInfo.multiaddrs.map((ma) => ma.toString())).to.contain( + multiaddr(`/ip6/${ip6}/tcp/${tcp}`).toString() + ); + expect(peerInfo.multiaddrs.map((ma) => ma.toString())).to.contain( + multiaddr(`/ip4/${ip4}/udp/${udp}`).toString() + ); + expect(peerInfo.multiaddrs.map((ma) => ma.toString())).to.contain( + multiaddr(`/ip6/${ip6}/udp/${udp}`).toString() + ); + }); }); describe("waku2 key round trip", async () => { let peerId; let enr: ENR; let waku2Protocols: Waku2; - let keypair: IKeypair; + let privateKey: Uint8Array; beforeEach(async function () { peerId = await createSecp256k1PeerId(); - enr = await ENR.createFromPeerId(peerId); - keypair = await createKeypairFromPeerId(peerId); + enr = await EnrCreator.fromPeerId(peerId); + privateKey = await getPrivateKeyFromPeerId(peerId); waku2Protocols = { relay: false, store: false, @@ -401,8 +428,8 @@ describe("ENR", function () { it("should set field with all protocols disabled", async () => { enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -417,8 +444,8 @@ describe("ENR", function () { waku2Protocols.lightPush = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(true); expect(decoded.store).to.equal(true); @@ -430,8 +457,8 @@ describe("ENR", function () { waku2Protocols.relay = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(true); expect(decoded.store).to.equal(false); @@ -443,8 +470,8 @@ describe("ENR", function () { waku2Protocols.store = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(true); @@ -456,8 +483,8 @@ describe("ENR", function () { waku2Protocols.filter = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -469,8 +496,8 @@ describe("ENR", function () { waku2Protocols.lightPush = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(keypair.privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const txt = await EnrEncoder.toString(enr, privateKey); + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -484,7 +511,7 @@ describe("ENR", function () { const txt = "enr:-Iu4QADPfXNCM6iYyte0pIdbMirIw_AsKR7J1DeJBysXDWz4DZvyjgIwpMt-sXTVUzLJdE9FaStVy2ZKtHUVQAH61-KAgmlkgnY0gmlwhMCosvuJc2VjcDI1NmsxoQI0OCNtPJtBayNgvFvKp-0YyCozcvE1rqm_V1W51nHVv4N0Y3CC6mCFd2FrdTIH"; - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(true); expect(decoded.store).to.equal(true); diff --git a/packages/enr/src/enr.ts b/packages/enr/src/enr.ts index 337bb06a43..60d949277f 100644 --- a/packages/enr/src/enr.ts +++ b/packages/enr/src/enr.ts @@ -1,58 +1,38 @@ -import * as RLP from "@ethersproject/rlp"; import type { PeerId } from "@libp2p/interface-peer-id"; +import type { PeerInfo } from "@libp2p/interface-peer-info"; import type { Multiaddr } from "@multiformats/multiaddr"; -import { - convertToBytes, - convertToString, -} from "@multiformats/multiaddr/convert"; import type { ENRKey, ENRValue, IEnr, NodeId, SequenceNumber, - Waku2, } from "@waku/interfaces"; -import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils"; import debug from "debug"; -import { fromString } from "uint8arrays/from-string"; -import { toString } from "uint8arrays/to-string"; -import { - ERR_INVALID_ID, - ERR_NO_SIGNATURE, - MAX_RECORD_SIZE, -} from "./constants.js"; -import { compressPublicKey, keccak256, verifySignature } from "./crypto.js"; -import { - createKeypair, - createKeypairFromPeerId, - createPeerIdFromKeypair, - IKeypair, - KeypairType, -} from "./keypair/index.js"; -import { multiaddrFromFields } from "./multiaddr_from_fields.js"; -import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec.js"; +import { ERR_INVALID_ID } from "./constants.js"; +import { keccak256, verifySignature } from "./crypto.js"; +import { locationMultiaddrFromEnrFields } from "./get_multiaddr.js"; +import { createPeerIdFromPublicKey } from "./peer_id.js"; +import { RawEnr } from "./raw_enr.js"; import * as v4 from "./v4.js"; -import { decodeWaku2, encodeWaku2 } from "./waku2_codec.js"; const log = debug("waku:enr"); -export class ENR extends Map implements IEnr { - public static readonly RECORD_PREFIX = "enr:"; - public seq: SequenceNumber; - public signature?: Uint8Array; - public peerId?: PeerId; +export enum TransportProtocol { + TCP = "tcp", + UDP = "udp", +} +export enum TransportProtocolPerIpVersion { + TCP4 = "tcp4", + UDP4 = "udp4", + TCP6 = "tcp6", + UDP6 = "udp6", +} - private constructor( - kvs: Record = {}, - seq: SequenceNumber = BigInt(1), - signature?: Uint8Array - ) { - super(Object.entries(kvs)); - this.seq = seq; - this.signature = signature; - } +export class ENR extends RawEnr implements IEnr { + public static readonly RECORD_PREFIX = "enr:"; + public peerId?: PeerId; static async create( kvs: Record = {}, @@ -63,8 +43,7 @@ export class ENR extends Map implements IEnr { try { const publicKey = enr.publicKey; if (publicKey) { - const keypair = createKeypair(enr.keypairType, undefined, publicKey); - enr.peerId = await createPeerIdFromKeypair(keypair); + enr.peerId = await createPeerIdFromPublicKey(publicKey); } } catch (e) { log("Could not calculate peer id for ENR", e); @@ -73,122 +52,6 @@ export class ENR extends Map implements IEnr { return enr; } - static createV4( - publicKey: Uint8Array, - kvs: Record = {} - ): Promise { - // EIP-778 specifies that the key must be in compressed format, 33 bytes - if (publicKey.length !== 33) { - publicKey = compressPublicKey(publicKey); - } - return ENR.create({ - ...kvs, - id: utf8ToBytes("v4"), - secp256k1: publicKey, - }); - } - - static async createFromPeerId( - peerId: PeerId, - kvs: Record = {} - ): Promise { - const keypair = await createKeypairFromPeerId(peerId); - switch (keypair.type) { - case KeypairType.secp256k1: - return ENR.createV4(keypair.publicKey, kvs); - default: - throw new Error(); - } - } - - static async decodeFromValues(decoded: Uint8Array[]): Promise { - if (!Array.isArray(decoded)) { - throw new Error("Decoded ENR must be an array"); - } - if (decoded.length % 2 !== 0) { - throw new Error("Decoded ENR must have an even number of elements"); - } - const [signature, seq, ...kvs] = decoded; - if (!signature || Array.isArray(signature)) { - throw new Error("Decoded ENR invalid signature: must be a byte array"); - } - if (!seq || Array.isArray(seq)) { - throw new Error( - "Decoded ENR invalid sequence number: must be a byte array" - ); - } - const obj: Record = {}; - for (let i = 0; i < kvs.length; i += 2) { - try { - obj[bytesToUtf8(kvs[i])] = kvs[i + 1]; - } catch (e) { - log("Failed to decode ENR key to UTF-8, skipping it", kvs[i], e); - } - } - // If seq is an empty array, translate as value 0 - const hexSeq = "0x" + (seq.length ? bytesToHex(seq) : "00"); - - const enr = await ENR.create(obj, BigInt(hexSeq), signature); - - const rlpEncodedBytes = hexToBytes(RLP.encode([seq, ...kvs])); - if (!enr.verify(rlpEncodedBytes, signature)) { - throw new Error("Unable to verify ENR signature"); - } - return enr; - } - - static decode(encoded: Uint8Array): Promise { - const decoded = RLP.decode(encoded).map(hexToBytes); - return ENR.decodeFromValues(decoded); - } - - static decodeTxt(encoded: string): Promise { - if (!encoded.startsWith(this.RECORD_PREFIX)) { - throw new Error( - `"string encoded ENR must start with '${this.RECORD_PREFIX}'` - ); - } - return ENR.decode(fromString(encoded.slice(4), "base64url")); - } - - set(k: ENRKey, v: ENRValue): this { - this.signature = undefined; - this.seq++; - return super.set(k, v); - } - - get id(): string { - const id = this.get("id"); - if (!id) throw new Error("id not found."); - return bytesToUtf8(id); - } - - get keypairType(): KeypairType { - switch (this.id) { - case "v4": - return KeypairType.secp256k1; - default: - throw new Error(ERR_INVALID_ID); - } - } - - get publicKey(): Uint8Array | undefined { - switch (this.id) { - case "v4": - return this.get("secp256k1"); - default: - throw new Error(ERR_INVALID_ID); - } - } - - get keypair(): IKeypair | undefined { - if (this.publicKey) { - const publicKey = this.publicKey; - return createKeypair(this.keypairType, undefined, publicKey); - } - return; - } - get nodeId(): NodeId | undefined { switch (this.id) { case "v4": @@ -197,193 +60,9 @@ export class ENR extends Map implements IEnr { throw new Error(ERR_INVALID_ID); } } - - get ip(): string | undefined { - const raw = this.get("ip"); - if (raw) { - return convertToString("ip4", raw) as string; - } else { - return undefined; - } - } - - set ip(ip: string | undefined) { - if (ip) { - this.set("ip", convertToBytes("ip4", ip)); - } else { - this.delete("ip"); - } - } - - get tcp(): number | undefined { - const raw = this.get("tcp"); - if (raw) { - return Number(convertToString("tcp", raw)); - } else { - return undefined; - } - } - - set tcp(port: number | undefined) { - if (port === undefined) { - this.delete("tcp"); - } else { - this.set("tcp", convertToBytes("tcp", port.toString(10))); - } - } - - get udp(): number | undefined { - const raw = this.get("udp"); - if (raw) { - return Number(convertToString("udp", raw)); - } else { - return undefined; - } - } - - set udp(port: number | undefined) { - if (port === undefined) { - this.delete("udp"); - } else { - this.set("udp", convertToBytes("udp", port.toString(10))); - } - } - - get ip6(): string | undefined { - const raw = this.get("ip6"); - if (raw) { - return convertToString("ip6", raw) as string; - } else { - return undefined; - } - } - - set ip6(ip: string | undefined) { - if (ip) { - this.set("ip6", convertToBytes("ip6", ip)); - } else { - this.delete("ip6"); - } - } - - get tcp6(): number | undefined { - const raw = this.get("tcp6"); - if (raw) { - return Number(convertToString("tcp", raw)); - } else { - return undefined; - } - } - - set tcp6(port: number | undefined) { - if (port === undefined) { - this.delete("tcp6"); - } else { - this.set("tcp6", convertToBytes("tcp", port.toString(10))); - } - } - - get udp6(): number | undefined { - const raw = this.get("udp6"); - if (raw) { - return Number(convertToString("udp", raw)); - } else { - return undefined; - } - } - - set udp6(port: number | undefined) { - if (port === undefined) { - this.delete("udp6"); - } else { - this.set("udp6", convertToBytes("udp", port.toString(10))); - } - } - - /** - * Get the `multiaddrs` field from ENR. - * - * This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys. - * These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host - * address (e.g. `dns4`, `dnsaddr`, etc).. - * - * If the peer information only contains information that can be represented with the ENR pre-defined keys - * (ip, tcp, etc) then the usage of { @link getLocationMultiaddr } should be preferred. - * - * The multiaddresses stored in this field are expected to be location multiaddresses, ie, peer id less. - */ - get multiaddrs(): Multiaddr[] | undefined { - const raw = this.get("multiaddrs"); - - if (raw) return decodeMultiaddrs(raw); - - return; - } - - /** - * Set the `multiaddrs` field on the ENR. - * - * This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys. - * These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host - * address (e.g. `dns4`, `dnsaddr`, etc).. - * - * If the peer information only contains information that can be represented with the ENR pre-defined keys - * (ip, tcp, etc) then the usage of { @link setLocationMultiaddr } should be preferred. - * The multiaddresses stored in this field must be location multiaddresses, - * ie, without a peer id. - */ - set multiaddrs(multiaddrs: Multiaddr[] | undefined) { - if (multiaddrs === undefined) { - this.delete("multiaddrs"); - } else { - const multiaddrsBuf = encodeMultiaddrs(multiaddrs); - this.set("multiaddrs", multiaddrsBuf); - } - } - - getLocationMultiaddr( - protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6" - ): Multiaddr | undefined { - if (protocol === "udp") { - return ( - this.getLocationMultiaddr("udp4") || this.getLocationMultiaddr("udp6") - ); - } - if (protocol === "tcp") { - return ( - this.getLocationMultiaddr("tcp4") || this.getLocationMultiaddr("tcp6") - ); - } - const isIpv6 = protocol.endsWith("6"); - const ipVal = this.get(isIpv6 ? "ip6" : "ip"); - if (!ipVal) { - return; - } - - const isUdp = protocol.startsWith("udp"); - const isTcp = protocol.startsWith("tcp"); - let protoName, protoVal; - if (isUdp) { - protoName = "udp"; - protoVal = isIpv6 ? this.get("udp6") : this.get("udp"); - } else if (isTcp) { - protoName = "tcp"; - protoVal = isIpv6 ? this.get("tcp6") : this.get("tcp"); - } else { - return; - } - - if (!protoVal) { - return; - } - - return multiaddrFromFields( - isIpv6 ? "ip6" : "ip4", - protoName, - ipVal, - protoVal - ); - } + getLocationMultiaddr: ( + protocol: TransportProtocol | TransportProtocolPerIpVersion + ) => Multiaddr | undefined = locationMultiaddrFromEnrFields.bind({}, this); setLocationMultiaddr(multiaddr: Multiaddr): void { const protoNames = multiaddr.protoNames(); @@ -409,6 +88,32 @@ export class ENR extends Map implements IEnr { } } + getAllLocationMultiaddrs(): Multiaddr[] { + const multiaddrs = []; + + for (const protocol of Object.values(TransportProtocolPerIpVersion)) { + const ma = this.getLocationMultiaddr( + protocol as TransportProtocolPerIpVersion + ); + if (ma) multiaddrs.push(ma); + } + + const _multiaddrs = this.multiaddrs ?? []; + multiaddrs.concat(_multiaddrs); + + return multiaddrs; + } + + get peerInfo(): PeerInfo | undefined { + const id = this.peerId; + if (!id) return; + return { + id, + multiaddrs: this.getAllLocationMultiaddrs(), + protocols: [], + }; + } + /** * Returns the full multiaddr from the ENR fields matching the provided * `protocol` parameter. @@ -418,7 +123,7 @@ export class ENR extends Map implements IEnr { * @param protocol */ getFullMultiaddr( - protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6" + protocol: TransportProtocol | TransportProtocolPerIpVersion ): Multiaddr | undefined { if (this.peerId) { const locationMultiaddr = this.getLocationMultiaddr(protocol); @@ -442,28 +147,6 @@ export class ENR extends Map implements IEnr { return []; } - /** - * Get the `waku2` field from ENR. - */ - get waku2(): Waku2 | undefined { - const raw = this.get("waku2"); - if (raw) return decodeWaku2(raw[0]); - - return; - } - - /** - * Set the `waku2` field on the ENR. - */ - set waku2(waku2: Waku2 | undefined) { - if (waku2 === undefined) { - this.delete("waku2"); - } else { - const byte = encodeWaku2(waku2); - this.set("waku2", new Uint8Array([byte])); - } - } - verify(data: Uint8Array, signature: Uint8Array): boolean { if (!this.get("id") || this.id !== "v4") { throw new Error(ERR_INVALID_ID); @@ -484,43 +167,4 @@ export class ENR extends Map implements IEnr { } return this.signature; } - - async encodeToValues( - privateKey?: Uint8Array - ): Promise<(ENRKey | ENRValue | number[])[]> { - // sort keys and flatten into [k, v, k, v, ...] - const content: Array = Array.from(this.keys()) - .sort((a, b) => a.localeCompare(b)) - .map((k) => [k, this.get(k)] as [ENRKey, ENRValue]) - .map(([k, v]) => [utf8ToBytes(k), v]) - .flat(); - content.unshift(new Uint8Array([Number(this.seq)])); - if (privateKey) { - content.unshift( - await this.sign(hexToBytes(RLP.encode(content)), privateKey) - ); - } else { - if (!this.signature) { - throw new Error(ERR_NO_SIGNATURE); - } - content.unshift(this.signature); - } - return content; - } - - async encode(privateKey?: Uint8Array): Promise { - const encoded = hexToBytes( - RLP.encode(await this.encodeToValues(privateKey)) - ); - if (encoded.length >= MAX_RECORD_SIZE) { - throw new Error("ENR must be less than 300 bytes"); - } - return encoded; - } - - async encodeTxt(privateKey?: Uint8Array): Promise { - return ( - ENR.RECORD_PREFIX + toString(await this.encode(privateKey), "base64url") - ); - } } diff --git a/packages/enr/src/get_multiaddr.ts b/packages/enr/src/get_multiaddr.ts new file mode 100644 index 0000000000..1f28fac2ac --- /dev/null +++ b/packages/enr/src/get_multiaddr.ts @@ -0,0 +1,47 @@ +import { Multiaddr } from "@multiformats/multiaddr"; +import type { IEnr } from "@waku/interfaces"; + +import { multiaddrFromFields } from "./multiaddr_from_fields.js"; + +export function locationMultiaddrFromEnrFields( + enr: IEnr, + protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6" +): Multiaddr | undefined { + switch (protocol) { + case "udp": + return ( + locationMultiaddrFromEnrFields(enr, "udp4") || + locationMultiaddrFromEnrFields(enr, "udp6") + ); + case "tcp": + return ( + locationMultiaddrFromEnrFields(enr, "tcp4") || + locationMultiaddrFromEnrFields(enr, "tcp6") + ); + } + const isIpv6 = protocol.endsWith("6"); + const ipVal = enr.get(isIpv6 ? "ip6" : "ip"); + if (!ipVal) return; + + const protoName = protocol.slice(0, 3); + let protoVal; + switch (protoName) { + case "udp": + protoVal = isIpv6 ? enr.get("udp6") : enr.get("udp"); + break; + case "tcp": + protoVal = isIpv6 ? enr.get("tcp6") : enr.get("tcp"); + break; + default: + return; + } + + if (!protoVal) return; + + return multiaddrFromFields( + isIpv6 ? "ip6" : "ip4", + protoName, + ipVal, + protoVal + ); +} diff --git a/packages/enr/src/index.ts b/packages/enr/src/index.ts index fc8d4c6203..d8b6fb5481 100644 --- a/packages/enr/src/index.ts +++ b/packages/enr/src/index.ts @@ -1,5 +1,7 @@ export * from "./constants.js"; +export * from "./creator.js"; +export * from "./decoder.js"; export * from "./enr.js"; -export * from "./keypair/index.js"; +export * from "./peer_id.js"; export * from "./waku2_codec.js"; export * from "./crypto.js"; diff --git a/packages/enr/src/keypair/index.ts b/packages/enr/src/keypair/index.ts deleted file mode 100644 index addd1dad34..0000000000 --- a/packages/enr/src/keypair/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { unmarshalPrivateKey, unmarshalPublicKey } from "@libp2p/crypto/keys"; -import { supportedKeys } from "@libp2p/crypto/keys"; -import type { PeerId } from "@libp2p/interface-peer-id"; -import { peerIdFromKeys } from "@libp2p/peer-id"; - -import { Secp256k1Keypair } from "./secp256k1.js"; -import { IKeypair, KeypairType } from "./types.js"; - -export const ERR_TYPE_NOT_IMPLEMENTED = "Keypair type not implemented"; -export * from "./types.js"; -export * from "./secp256k1.js"; - -export function createKeypair( - type: KeypairType, - privateKey?: Uint8Array, - publicKey?: Uint8Array -): IKeypair { - switch (type) { - case KeypairType.secp256k1: - return new Secp256k1Keypair(privateKey, publicKey); - default: - throw new Error(ERR_TYPE_NOT_IMPLEMENTED); - } -} - -export async function createPeerIdFromKeypair( - keypair: IKeypair -): Promise { - switch (keypair.type) { - case KeypairType.secp256k1: { - const publicKey = new supportedKeys.secp256k1.Secp256k1PublicKey( - keypair.publicKey - ); - - const privateKey = keypair.hasPrivateKey() - ? new supportedKeys.secp256k1.Secp256k1PrivateKey(keypair.privateKey) - : undefined; - - return peerIdFromKeys(publicKey.bytes, privateKey?.bytes); - } - default: - throw new Error(ERR_TYPE_NOT_IMPLEMENTED); - } -} - -export async function createKeypairFromPeerId( - peerId: PeerId -): Promise { - let keypairType; - switch (peerId.type) { - case "RSA": - keypairType = KeypairType.rsa; - break; - case "Ed25519": - keypairType = KeypairType.ed25519; - break; - case "secp256k1": - keypairType = KeypairType.secp256k1; - break; - default: - throw new Error("Unsupported peer id type"); - } - - const publicKey = peerId.publicKey - ? unmarshalPublicKey(peerId.publicKey) - : undefined; - const privateKey = peerId.privateKey - ? await unmarshalPrivateKey(peerId.privateKey) - : undefined; - - return createKeypair( - keypairType, - privateKey?.marshal(), - publicKey?.marshal() - ); -} diff --git a/packages/enr/src/keypair/secp256k1.ts b/packages/enr/src/keypair/secp256k1.ts deleted file mode 100644 index b658cb5421..0000000000 --- a/packages/enr/src/keypair/secp256k1.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as secp from "@noble/secp256k1"; - -import { compressPublicKey, randomBytes } from "../crypto.js"; - -import { IKeypair, KeypairType } from "./types.js"; - -export class Secp256k1Keypair implements IKeypair { - readonly type: KeypairType; - _privateKey?: Uint8Array; - readonly _publicKey?: Uint8Array; - - constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) { - let pub = publicKey; - if (pub) { - pub = compressPublicKey(pub); - } - if ((this._privateKey = privateKey) && !this.privateKeyVerify()) { - throw new Error("Invalid private key"); - } - if ((this._publicKey = pub) && !this.publicKeyVerify()) { - throw new Error("Invalid public key"); - } - - this.type = KeypairType.secp256k1; - } - - static async generate(): Promise { - const privateKey = randomBytes(32); - const publicKey = secp.getPublicKey(privateKey); - return new Secp256k1Keypair(privateKey, publicKey); - } - - privateKeyVerify(key = this._privateKey): boolean { - if (key) { - return secp.utils.isValidPrivateKey(key); - } - return true; - } - - publicKeyVerify(key = this._publicKey): boolean { - if (key) { - try { - secp.Point.fromHex(key); - return true; - } catch { - return false; - } - } - return true; - } - - get privateKey(): Uint8Array { - if (!this._privateKey) { - throw new Error(); - } - return this._privateKey; - } - - get publicKey(): Uint8Array { - if (!this._publicKey) { - throw new Error(); - } - return this._publicKey; - } - - hasPrivateKey(): boolean { - return !!this._privateKey; - } -} diff --git a/packages/enr/src/keypair/types.ts b/packages/enr/src/keypair/types.ts deleted file mode 100644 index a24618dadf..0000000000 --- a/packages/enr/src/keypair/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum KeypairType { - rsa = 0, - ed25519 = 1, - secp256k1 = 2, -} - -export interface IKeypair { - type: KeypairType; - privateKey: Uint8Array; - publicKey: Uint8Array; - privateKeyVerify(): boolean; - publicKeyVerify(): boolean; - hasPrivateKey(): boolean; -} diff --git a/packages/enr/src/peer_id.ts b/packages/enr/src/peer_id.ts new file mode 100644 index 0000000000..db65f14d2a --- /dev/null +++ b/packages/enr/src/peer_id.ts @@ -0,0 +1,34 @@ +import { unmarshalPrivateKey, unmarshalPublicKey } from "@libp2p/crypto/keys"; +import { supportedKeys } from "@libp2p/crypto/keys"; +import type { PeerId } from "@libp2p/interface-peer-id"; +import { peerIdFromKeys } from "@libp2p/peer-id"; + +export function createPeerIdFromPublicKey( + publicKey: Uint8Array +): Promise { + const _publicKey = new supportedKeys.secp256k1.Secp256k1PublicKey(publicKey); + return peerIdFromKeys(_publicKey.bytes, undefined); +} + +export function getPublicKeyFromPeerId(peerId: PeerId): Uint8Array { + if (peerId.type !== "secp256k1") { + throw new Error("Unsupported peer id type"); + } + + return unmarshalPublicKey(peerId.publicKey).marshal(); +} + +// Only used in tests +export async function getPrivateKeyFromPeerId( + peerId: PeerId +): Promise { + if (peerId.type !== "secp256k1") { + throw new Error("Unsupported peer id type"); + } + if (!peerId.privateKey) { + throw new Error("Private key not present on peer id"); + } + + const privateKey = await unmarshalPrivateKey(peerId.privateKey); + return privateKey.marshal(); +} diff --git a/packages/enr/src/raw_enr.ts b/packages/enr/src/raw_enr.ts new file mode 100644 index 0000000000..73c00bca30 --- /dev/null +++ b/packages/enr/src/raw_enr.ts @@ -0,0 +1,212 @@ +import type { Multiaddr } from "@multiformats/multiaddr"; +import { + convertToBytes, + convertToString, +} from "@multiformats/multiaddr/convert"; +import type { ENRKey, ENRValue, SequenceNumber, Waku2 } from "@waku/interfaces"; +import { bytesToUtf8 } from "@waku/utils"; + +import { ERR_INVALID_ID } from "./constants.js"; +import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec.js"; +import { decodeWaku2, encodeWaku2 } from "./waku2_codec.js"; + +export class RawEnr extends Map { + public seq: SequenceNumber; + public signature?: Uint8Array; + + protected constructor( + kvs: Record = {}, + seq: SequenceNumber = BigInt(1), + signature?: Uint8Array + ) { + super(Object.entries(kvs)); + this.seq = seq; + this.signature = signature; + } + + set(k: ENRKey, v: ENRValue): this { + this.signature = undefined; + this.seq++; + return super.set(k, v); + } + + get id(): string { + const id = this.get("id"); + if (!id) throw new Error("id not found."); + return bytesToUtf8(id); + } + + get publicKey(): Uint8Array | undefined { + switch (this.id) { + case "v4": + return this.get("secp256k1"); + default: + throw new Error(ERR_INVALID_ID); + } + } + + get ip(): string | undefined { + const raw = this.get("ip"); + if (raw) { + return convertToString("ip4", raw) as string; + } else { + return undefined; + } + } + + set ip(ip: string | undefined) { + if (ip) { + this.set("ip", convertToBytes("ip4", ip)); + } else { + this.delete("ip"); + } + } + + get tcp(): number | undefined { + const raw = this.get("tcp"); + if (raw) { + return Number(convertToString("tcp", raw)); + } else { + return undefined; + } + } + + set tcp(port: number | undefined) { + if (port === undefined) { + this.delete("tcp"); + } else { + this.set("tcp", convertToBytes("tcp", port.toString(10))); + } + } + + get udp(): number | undefined { + const raw = this.get("udp"); + if (raw) { + return Number(convertToString("udp", raw)); + } else { + return undefined; + } + } + + set udp(port: number | undefined) { + if (port === undefined) { + this.delete("udp"); + } else { + this.set("udp", convertToBytes("udp", port.toString(10))); + } + } + + get ip6(): string | undefined { + const raw = this.get("ip6"); + if (raw) { + return convertToString("ip6", raw) as string; + } else { + return undefined; + } + } + + set ip6(ip: string | undefined) { + if (ip) { + this.set("ip6", convertToBytes("ip6", ip)); + } else { + this.delete("ip6"); + } + } + + get tcp6(): number | undefined { + const raw = this.get("tcp6"); + if (raw) { + return Number(convertToString("tcp", raw)); + } else { + return undefined; + } + } + + set tcp6(port: number | undefined) { + if (port === undefined) { + this.delete("tcp6"); + } else { + this.set("tcp6", convertToBytes("tcp", port.toString(10))); + } + } + + get udp6(): number | undefined { + const raw = this.get("udp6"); + if (raw) { + return Number(convertToString("udp", raw)); + } else { + return undefined; + } + } + + set udp6(port: number | undefined) { + if (port === undefined) { + this.delete("udp6"); + } else { + this.set("udp6", convertToBytes("udp", port.toString(10))); + } + } + + /** + * Get the `multiaddrs` field from ENR. + * + * This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys. + * These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host + * address (e.g. `dns4`, `dnsaddr`, etc).. + * + * If the peer information only contains information that can be represented with the ENR pre-defined keys + * (ip, tcp, etc) then the usage of { @link ENR.getLocationMultiaddr } should be preferred. + * + * The multiaddresses stored in this field are expected to be location multiaddresses, ie, peer id less. + */ + get multiaddrs(): Multiaddr[] | undefined { + const raw = this.get("multiaddrs"); + + if (raw) return decodeMultiaddrs(raw); + + return; + } + + /** + * Set the `multiaddrs` field on the ENR. + * + * This field is used to store multiaddresses that cannot be stored with the current ENR pre-defined keys. + * These can be a multiaddresses that include encapsulation (e.g. wss) or do not use `ip4` nor `ip6` for the host + * address (e.g. `dns4`, `dnsaddr`, etc).. + * + * If the peer information only contains information that can be represented with the ENR pre-defined keys + * (ip, tcp, etc) then the usage of { @link ENR.setLocationMultiaddr } should be preferred. + * The multiaddresses stored in this field must be location multiaddresses, + * ie, without a peer id. + */ + set multiaddrs(multiaddrs: Multiaddr[] | undefined) { + if (multiaddrs === undefined) { + this.delete("multiaddrs"); + } else { + const multiaddrsBuf = encodeMultiaddrs(multiaddrs); + this.set("multiaddrs", multiaddrsBuf); + } + } + + /** + * Get the `waku2` field from ENR. + */ + get waku2(): Waku2 | undefined { + const raw = this.get("waku2"); + if (raw) return decodeWaku2(raw[0]); + + return; + } + + /** + * Set the `waku2` field on the ENR. + */ + set waku2(waku2: Waku2 | undefined) { + if (waku2 === undefined) { + this.delete("waku2"); + } else { + const byte = encodeWaku2(waku2); + this.set("waku2", new Uint8Array([byte])); + } + } +} diff --git a/packages/enr/src/v4.ts b/packages/enr/src/v4.ts index 7964a787ca..e1d3b1666f 100644 --- a/packages/enr/src/v4.ts +++ b/packages/enr/src/v4.ts @@ -3,7 +3,6 @@ import type { NodeId } from "@waku/interfaces"; import { bytesToHex } from "@waku/utils"; import { keccak256 } from "./crypto.js"; - export async function sign( privKey: Uint8Array, msg: Uint8Array diff --git a/packages/interfaces/package.json b/packages/interfaces/package.json index dc1c4b1221..b8f4e101b0 100644 --- a/packages/interfaces/package.json +++ b/packages/interfaces/package.json @@ -54,6 +54,7 @@ "@libp2p/interface-connection-manager": "^1.3.7", "@libp2p/interface-libp2p": "^1.1.1", "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", "@libp2p/interface-registrar": "^2.0.8", "@multiformats/multiaddr": "^11.4.0", diff --git a/packages/interfaces/src/enr.ts b/packages/interfaces/src/enr.ts index 33dcbe28e2..d15f87af37 100644 --- a/packages/interfaces/src/enr.ts +++ b/packages/interfaces/src/enr.ts @@ -1,4 +1,5 @@ import type { PeerId } from "@libp2p/interface-peer-id"; +import type { PeerInfo } from "@libp2p/interface-peer-info"; import type { Multiaddr } from "@multiformats/multiaddr"; export type ENRKey = string; @@ -32,7 +33,10 @@ export interface IEnr extends Map { udp6?: number; multiaddrs?: Multiaddr[]; waku2?: Waku2; + peerInfo: PeerInfo | undefined; - encode(privateKey?: Uint8Array): Promise; + /** + * @deprecated: use { @link IEnr.peerInfo } instead. + */ getFullMultiaddrs(): Multiaddr[]; } diff --git a/packages/peer-exchange/src/waku_peer_exchange.ts b/packages/peer-exchange/src/waku_peer_exchange.ts index 9fedc0d96c..afe07dba1f 100644 --- a/packages/peer-exchange/src/waku_peer_exchange.ts +++ b/packages/peer-exchange/src/waku_peer_exchange.ts @@ -5,7 +5,7 @@ import type { Registrar, } from "@libp2p/interface-registrar"; import { BaseProtocol } from "@waku/core/lib/base_protocol"; -import { ENR } from "@waku/enr"; +import { EnrDecoder } from "@waku/enr"; import type { IPeerExchange, PeerExchangeQueryParams, @@ -95,7 +95,7 @@ export class WakuPeerExchange extends BaseProtocol implements IPeerExchange { const enrs = await Promise.all( decoded.peerInfos.map( - (peerInfo) => peerInfo.enr && ENR.decode(peerInfo.enr) + (peerInfo) => peerInfo.enr && EnrDecoder.fromRLP(peerInfo.enr) ) ); diff --git a/packages/peer-exchange/src/waku_peer_exchange_discovery.ts b/packages/peer-exchange/src/waku_peer_exchange_discovery.ts index 72b1a3a241..c0c46ccf45 100644 --- a/packages/peer-exchange/src/waku_peer_exchange_discovery.ts +++ b/packages/peer-exchange/src/waku_peer_exchange_discovery.ts @@ -49,8 +49,8 @@ export interface Options { } export const DEFAULT_PEER_EXCHANGE_TAG_NAME = "peer-exchange"; -const DEFAULT_PEER_EXCHANGE_BOOTSTRAP_TAG_VALUE = 50; -const DEFAULT_PEER_EXCHANGE_BOOTSTRAP_TAG_TTL = 120000; +const DEFAULT_PEER_EXCHANGE_TAG_VALUE = 50; +const DEFAULT_PEER_EXCHANGE_TAG_TTL = 120000; export class PeerExchangeDiscovery extends EventEmitter @@ -165,37 +165,35 @@ export class PeerExchangeDiscovery continue; } - const { peerId } = ENR; - const multiaddrs = ENR.getFullMultiaddrs(); - - if (!peerId || !multiaddrs || multiaddrs.length === 0) continue; + const peerInfo = ENR.peerInfo; if ( - (await this.components.peerStore.getTags(peerId)).find( + !peerInfo || + !peerInfo.id || + !peerInfo.multiaddrs || + !peerInfo.multiaddrs.length + ) + continue; + + if ( + (await this.components.peerStore.getTags(peerInfo.id)).find( ({ name }) => name === DEFAULT_PEER_EXCHANGE_TAG_NAME ) ) continue; await this.components.peerStore.tagPeer( - peerId, + peerInfo.id, DEFAULT_PEER_EXCHANGE_TAG_NAME, { - value: - this.options.tagValue ?? - DEFAULT_PEER_EXCHANGE_BOOTSTRAP_TAG_VALUE, - ttl: - this.options.tagTTL ?? DEFAULT_PEER_EXCHANGE_BOOTSTRAP_TAG_TTL, + value: this.options.tagValue ?? DEFAULT_PEER_EXCHANGE_TAG_VALUE, + ttl: this.options.tagTTL ?? DEFAULT_PEER_EXCHANGE_TAG_TTL, } ); this.dispatchEvent( new CustomEvent("peer", { - detail: { - id: peerId, - multiaddrs, - protocols: [], - }, + detail: peerInfo, }) ); } diff --git a/packages/tests/tests/enr.node.spec.ts b/packages/tests/tests/enr.node.spec.ts index 415e91aaa5..5e5fe8c111 100644 --- a/packages/tests/tests/enr.node.spec.ts +++ b/packages/tests/tests/enr.node.spec.ts @@ -1,6 +1,6 @@ import { waitForRemotePeer } from "@waku/core"; import { createRelayNode } from "@waku/create"; -import { ENR } from "@waku/enr"; +import { EnrDecoder } from "@waku/enr"; import type { RelayNode } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; import { expect } from "chai"; @@ -38,7 +38,7 @@ describe("ENR Interop: nwaku", function () { const nimPeerId = await nwaku.getPeerId(); expect(nwakuInfo.enrUri).to.not.be.undefined; - const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? ""); + const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? ""); expect(dec.peerId?.toString()).to.eq(nimPeerId.toString()); expect(dec.waku2).to.deep.eq({ relay: true, @@ -70,7 +70,7 @@ describe("ENR Interop: nwaku", function () { const nimPeerId = await nwaku.getPeerId(); expect(nwakuInfo.enrUri).to.not.be.undefined; - const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? ""); + const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? ""); expect(dec.peerId?.toString()).to.eq(nimPeerId.toString()); expect(dec.waku2).to.deep.eq({ relay: true, @@ -102,7 +102,7 @@ describe("ENR Interop: nwaku", function () { const nimPeerId = await nwaku.getPeerId(); expect(nwakuInfo.enrUri).to.not.be.undefined; - const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? ""); + const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? ""); expect(dec.peerId?.toString()).to.eq(nimPeerId.toString()); expect(dec.waku2).to.deep.eq({ relay: true, diff --git a/packages/utils/package.json b/packages/utils/package.json index 8515bf3b8e..b50133b30c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -50,16 +50,13 @@ "node": ">=16" }, "dependencies": { - "@libp2p/peer-id": "^2.0.2", "debug": "^4.3.4", "uint8arrays": "^4.0.3" }, "devDependencies": { "@libp2p/interface-connection": "^3.0.8", "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/interface-peer-info": "^1.0.8", "@libp2p/interface-peer-store": "^1.2.8", - "@multiformats/multiaddr": "^11.4.0", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/utils/src/libp2p/index.ts b/packages/utils/src/libp2p/index.ts index 13536c6ba5..a0ffefc974 100644 --- a/packages/utils/src/libp2p/index.ts +++ b/packages/utils/src/libp2p/index.ts @@ -1,9 +1,6 @@ import type { Connection } from "@libp2p/interface-connection"; import type { PeerId } from "@libp2p/interface-peer-id"; -import type { PeerInfo } from "@libp2p/interface-peer-info"; import type { Peer, PeerStore } from "@libp2p/interface-peer-store"; -import { peerIdFromString } from "@libp2p/peer-id"; -import type { Multiaddr } from "@multiformats/multiaddr"; import debug from "debug"; const log = debug("waku:libp2p-utils"); @@ -78,20 +75,6 @@ export async function selectPeerForProtocol( return { peer, protocol }; } -export function multiaddrsToPeerInfo(mas: Multiaddr[]): PeerInfo[] { - return mas - .map((ma) => { - const peerIdStr = ma.getPeerId(); - const protocols: string[] = []; - return { - id: peerIdStr ? peerIdFromString(peerIdStr) : null, - multiaddrs: [ma.decapsulateCode(421)], - protocols, - }; - }) - .filter((peerInfo): peerInfo is PeerInfo => peerInfo.id !== null); -} - export function selectConnection( connections: Connection[] ): Connection | undefined {