From 130c49b636807063364f309da0da2a24a68f2178 Mon Sep 17 00:00:00 2001 From: "fryorcraken.eth" Date: Fri, 3 Mar 2023 13:49:32 +1100 Subject: [PATCH] chore!: extract decoder code Separation of concerns by moving decoding logic in new class. --- packages/dns-discovery/src/dns.ts | 4 +- packages/enr/src/decoder.ts | 59 +++++++++++++++++++ packages/enr/src/enr.spec.ts | 33 ++++++----- packages/enr/src/enr.ts | 54 +---------------- packages/enr/src/index.ts | 1 + .../peer-exchange/src/waku_peer_exchange.ts | 4 +- packages/tests/tests/enr.node.spec.ts | 8 +-- 7 files changed, 86 insertions(+), 77 deletions(-) create mode 100644 packages/enr/src/decoder.ts 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/enr/src/decoder.ts b/packages/enr/src/decoder.ts new file mode 100644 index 0000000000..c2a2370f2d --- /dev/null +++ b/packages/enr/src/decoder.ts @@ -0,0 +1,59 @@ +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 EnrDecoder.fromValues(decoded); + } + + private static async fromValues(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; + } +} diff --git a/packages/enr/src/enr.spec.ts b/packages/enr/src/enr.spec.ts index 65e60dd6ea..8b1277aaf3 100644 --- a/packages/enr/src/enr.spec.ts +++ b/packages/enr/src/enr.spec.ts @@ -8,6 +8,7 @@ import { equals } from "uint8arrays/equals"; import { ERR_INVALID_ID } from "./constants.js"; import { EnrCreator } from "./creator.js"; +import { EnrDecoder } from "./decoder.js"; import { ENR } from "./enr.js"; import { getPrivateKeyFromPeerId } from "./peer_id.js"; @@ -34,7 +35,7 @@ describe("ENR", function () { }; const txt = await enr.encodeTxt(privateKey); - const enr2 = await ENR.decodeTxt(txt); + const enr2 = await EnrDecoder.fromString(txt); if (!enr.signature) throw "enr.signature is undefined"; if (!enr2.signature) throw "enr.signature is undefined"; @@ -65,7 +66,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"); @@ -74,7 +75,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); @@ -93,7 +94,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; @@ -114,7 +115,7 @@ describe("ENR", function () { enr.set("id", new Uint8Array([0])); const txt = await enr.encodeTxt(privateKey); - await ENR.decodeTxt(txt); + await EnrDecoder.fromString(txt); assert.fail("Expect error here"); } catch (err: unknown) { const e = err as Error; @@ -126,7 +127,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; @@ -180,7 +181,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; }); @@ -209,7 +210,7 @@ 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 decoded = await EnrDecoder.fromRLP(encoded); record.forEach((value, key) => { expect(equals(decoded.get(key)!, value)).to.be.true; @@ -220,7 +221,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 @@ -403,7 +404,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -419,7 +420,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - 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); @@ -432,7 +433,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - 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(false); @@ -445,7 +446,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(true); @@ -458,7 +459,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -471,7 +472,7 @@ describe("ENR", function () { enr.waku2 = waku2Protocols; const txt = await enr.encodeTxt(privateKey); - const decoded = (await ENR.decodeTxt(txt)).waku2!; + const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); expect(decoded.store).to.equal(false); @@ -485,7 +486,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 8ee793ab8f..5066fe6c4a 100644 --- a/packages/enr/src/enr.ts +++ b/packages/enr/src/enr.ts @@ -13,9 +13,8 @@ import type { SequenceNumber, Waku2, } from "@waku/interfaces"; -import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils"; +import { bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils"; import debug from "debug"; -import { fromString } from "uint8arrays/from-string"; import { toString } from "uint8arrays/to-string"; import { @@ -65,57 +64,6 @@ export class ENR extends Map implements IEnr { return enr; } - - 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++; diff --git a/packages/enr/src/index.ts b/packages/enr/src/index.ts index 496c7e82c2..d8b6fb5481 100644 --- a/packages/enr/src/index.ts +++ b/packages/enr/src/index.ts @@ -1,5 +1,6 @@ export * from "./constants.js"; export * from "./creator.js"; +export * from "./decoder.js"; export * from "./enr.js"; export * from "./peer_id.js"; export * from "./waku2_codec.js"; 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/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,