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 8b1277aaf3..6ad814e67c 100644 --- a/packages/enr/src/enr.spec.ts +++ b/packages/enr/src/enr.spec.ts @@ -9,6 +9,7 @@ import { equals } from "uint8arrays/equals"; import { ERR_INVALID_ID } from "./constants.js"; import { EnrCreator } from "./creator.js"; import { EnrDecoder } from "./decoder.js"; +import { EnrEncoder } from "./encoder.js"; import { ENR } from "./enr.js"; import { getPrivateKeyFromPeerId } from "./peer_id.js"; @@ -34,7 +35,7 @@ describe("ENR", function () { lightPush: false, }; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const enr2 = await EnrDecoder.fromString(txt); if (!enr.signature) throw "enr.signature is undefined"; @@ -113,7 +114,7 @@ describe("ENR", function () { enr.setLocationMultiaddr(multiaddr("/ip4/18.223.219.100/udp/9000")); enr.set("id", new Uint8Array([0])); - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); await EnrDecoder.fromString(txt); assert.fail("Expect error here"); @@ -199,7 +200,7 @@ describe("ENR", function () { 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", () => { @@ -209,7 +210,7 @@ describe("ENR", function () { }); it("should encode/decode to RLP encoding", async function () { - const encoded = await record.encode(privateKey); + const encoded = await EnrEncoder.toBytes(record, privateKey); const decoded = await EnrDecoder.fromRLP(encoded); record.forEach((value, key) => { @@ -403,7 +404,7 @@ describe("ENR", function () { it("should set field with all protocols disabled", async () => { enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); @@ -419,7 +420,7 @@ describe("ENR", function () { waku2Protocols.lightPush = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(true); @@ -432,7 +433,7 @@ describe("ENR", function () { waku2Protocols.relay = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(true); @@ -445,7 +446,7 @@ describe("ENR", function () { waku2Protocols.store = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); @@ -458,7 +459,7 @@ describe("ENR", function () { waku2Protocols.filter = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); @@ -471,7 +472,7 @@ describe("ENR", function () { waku2Protocols.lightPush = true; enr.waku2 = waku2Protocols; - const txt = await enr.encodeTxt(privateKey); + const txt = await EnrEncoder.toString(enr, privateKey); const decoded = (await EnrDecoder.fromString(txt)).waku2!; expect(decoded.relay).to.equal(false); diff --git a/packages/enr/src/enr.ts b/packages/enr/src/enr.ts index 5066fe6c4a..7e5e6160bc 100644 --- a/packages/enr/src/enr.ts +++ b/packages/enr/src/enr.ts @@ -1,4 +1,3 @@ -import * as RLP from "@ethersproject/rlp"; import type { PeerId } from "@libp2p/interface-peer-id"; import type { Multiaddr } from "@multiformats/multiaddr"; import { @@ -13,15 +12,10 @@ import type { SequenceNumber, Waku2, } from "@waku/interfaces"; -import { bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils"; +import { bytesToUtf8 } from "@waku/utils"; import debug from "debug"; -import { toString } from "uint8arrays/to-string"; -import { - ERR_INVALID_ID, - ERR_NO_SIGNATURE, - MAX_RECORD_SIZE, -} from "./constants.js"; +import { ERR_INVALID_ID } from "./constants.js"; import { keccak256, verifySignature } from "./crypto.js"; import { multiaddrFromFields } from "./multiaddr_from_fields.js"; import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec.js"; @@ -380,43 +374,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/interfaces/src/enr.ts b/packages/interfaces/src/enr.ts index 33dcbe28e2..c14cfe0a83 100644 --- a/packages/interfaces/src/enr.ts +++ b/packages/interfaces/src/enr.ts @@ -33,6 +33,5 @@ export interface IEnr extends Map { multiaddrs?: Multiaddr[]; waku2?: Waku2; - encode(privateKey?: Uint8Array): Promise; getFullMultiaddrs(): Multiaddr[]; }