Skip to content

Commit 130c49b

Browse files
committed
chore!: extract decoder code
Separation of concerns by moving decoding logic in new class.
1 parent 8fd1455 commit 130c49b

File tree

7 files changed

+86
-77
lines changed

7 files changed

+86
-77
lines changed

packages/dns-discovery/src/dns.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ENR } from "@waku/enr";
1+
import { ENR, EnrDecoder } from "@waku/enr";
22
import type { IEnr } from "@waku/interfaces";
33
import debug from "debug";
44

@@ -131,7 +131,7 @@ export class DnsNodeDiscovery {
131131
next = selectRandomPath(branches, context);
132132
return await this._search(next, context);
133133
case ENRTree.RECORD_PREFIX:
134-
return ENR.decodeTxt(entry);
134+
return EnrDecoder.fromString(entry);
135135
default:
136136
return null;
137137
}

packages/enr/src/decoder.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as RLP from "@ethersproject/rlp";
2+
import type { ENRKey, ENRValue } from "@waku/interfaces";
3+
import { bytesToHex, bytesToUtf8, hexToBytes } from "@waku/utils";
4+
import { log } from "debug";
5+
import { fromString } from "uint8arrays/from-string";
6+
7+
import { ENR } from "./enr.js";
8+
9+
export class EnrDecoder {
10+
static fromString(encoded: string): Promise<ENR> {
11+
if (!encoded.startsWith(ENR.RECORD_PREFIX)) {
12+
throw new Error(
13+
`"string encoded ENR must start with '${ENR.RECORD_PREFIX}'`
14+
);
15+
}
16+
return EnrDecoder.fromRLP(fromString(encoded.slice(4), "base64url"));
17+
}
18+
19+
static fromRLP(encoded: Uint8Array): Promise<ENR> {
20+
const decoded = RLP.decode(encoded).map(hexToBytes);
21+
return EnrDecoder.fromValues(decoded);
22+
}
23+
24+
private static async fromValues(decoded: Uint8Array[]): Promise<ENR> {
25+
if (!Array.isArray(decoded)) {
26+
throw new Error("Decoded ENR must be an array");
27+
}
28+
if (decoded.length % 2 !== 0) {
29+
throw new Error("Decoded ENR must have an even number of elements");
30+
}
31+
const [signature, seq, ...kvs] = decoded;
32+
if (!signature || Array.isArray(signature)) {
33+
throw new Error("Decoded ENR invalid signature: must be a byte array");
34+
}
35+
if (!seq || Array.isArray(seq)) {
36+
throw new Error(
37+
"Decoded ENR invalid sequence number: must be a byte array"
38+
);
39+
}
40+
const obj: Record<ENRKey, ENRValue> = {};
41+
for (let i = 0; i < kvs.length; i += 2) {
42+
try {
43+
obj[bytesToUtf8(kvs[i])] = kvs[i + 1];
44+
} catch (e) {
45+
log("Failed to decode ENR key to UTF-8, skipping it", kvs[i], e);
46+
}
47+
}
48+
// If seq is an empty array, translate as value 0
49+
const hexSeq = "0x" + (seq.length ? bytesToHex(seq) : "00");
50+
51+
const enr = await ENR.create(obj, BigInt(hexSeq), signature);
52+
53+
const rlpEncodedBytes = hexToBytes(RLP.encode([seq, ...kvs]));
54+
if (!enr.verify(rlpEncodedBytes, signature)) {
55+
throw new Error("Unable to verify ENR signature");
56+
}
57+
return enr;
58+
}
59+
}

packages/enr/src/enr.spec.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { equals } from "uint8arrays/equals";
88

99
import { ERR_INVALID_ID } from "./constants.js";
1010
import { EnrCreator } from "./creator.js";
11+
import { EnrDecoder } from "./decoder.js";
1112
import { ENR } from "./enr.js";
1213
import { getPrivateKeyFromPeerId } from "./peer_id.js";
1314

@@ -34,7 +35,7 @@ describe("ENR", function () {
3435
};
3536

3637
const txt = await enr.encodeTxt(privateKey);
37-
const enr2 = await ENR.decodeTxt(txt);
38+
const enr2 = await EnrDecoder.fromString(txt);
3839

3940
if (!enr.signature) throw "enr.signature is undefined";
4041
if (!enr2.signature) throw "enr.signature is undefined";
@@ -65,7 +66,7 @@ describe("ENR", function () {
6566
it("should decode valid enr successfully", async () => {
6667
const txt =
6768
"enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg";
68-
const enr = await ENR.decodeTxt(txt);
69+
const enr = await EnrDecoder.fromString(txt);
6970
const eth2 = enr.get("eth2");
7071
if (!eth2) throw "eth2 is undefined";
7172
expect(bytesToHex(eth2)).to.be.equal("f6775d0700000113ffffffffffff1f00");
@@ -74,7 +75,7 @@ describe("ENR", function () {
7475
it("should decode valid ENR with multiaddrs successfully [shared test vector]", async () => {
7576
const txt =
7677
"enr:-QEnuEBEAyErHEfhiQxAVQoWowGTCuEF9fKZtXSd7H_PymHFhGJA3rGAYDVSHKCyJDGRLBGsloNbS8AZF33IVuefjOO6BIJpZIJ2NIJpcIQS39tkim11bHRpYWRkcnO4lgAvNihub2RlLTAxLmRvLWFtczMud2FrdXYyLnRlc3Quc3RhdHVzaW0ubmV0BgG73gMAODcxbm9kZS0wMS5hYy1jbi1ob25na29uZy1jLndha3V2Mi50ZXN0LnN0YXR1c2ltLm5ldAYBu94DACm9A62t7AQL4Ef5ZYZosRpQTzFVAB8jGjf1TER2wH-0zBOe1-MDBNLeA4lzZWNwMjU2azGhAzfsxbxyCkgCqq8WwYsVWH7YkpMLnU2Bw5xJSimxKav-g3VkcIIjKA";
77-
const enr = await ENR.decodeTxt(txt);
78+
const enr = await EnrDecoder.fromString(txt);
7879

7980
expect(enr.multiaddrs).to.not.be.undefined;
8081
expect(enr.multiaddrs!.length).to.be.equal(3);
@@ -93,7 +94,7 @@ describe("ENR", function () {
9394
it("should decode valid enr with tcp successfully", async () => {
9495
const txt =
9596
"enr:-IS4QAmC_o1PMi5DbR4Bh4oHVyQunZblg4bTaottPtBodAhJZvxVlWW-4rXITPNg4mwJ8cW__D9FBDc9N4mdhyMqB-EBgmlkgnY0gmlwhIbRi9KJc2VjcDI1NmsxoQOevTdO6jvv3fRruxguKR-3Ge4bcFsLeAIWEDjrfaigNoN0Y3CCdl8";
96-
const enr = await ENR.decodeTxt(txt);
97+
const enr = await EnrDecoder.fromString(txt);
9798
expect(enr.tcp).to.not.be.undefined;
9899
expect(enr.tcp).to.be.equal(30303);
99100
expect(enr.ip).to.not.be.undefined;
@@ -114,7 +115,7 @@ describe("ENR", function () {
114115
enr.set("id", new Uint8Array([0]));
115116
const txt = await enr.encodeTxt(privateKey);
116117

117-
await ENR.decodeTxt(txt);
118+
await EnrDecoder.fromString(txt);
118119
assert.fail("Expect error here");
119120
} catch (err: unknown) {
120121
const e = err as Error;
@@ -126,7 +127,7 @@ describe("ENR", function () {
126127
try {
127128
const txt =
128129
"enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2d11eu6dCsxoQIB_c-jQMOXsbjWkbN-kj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk";
129-
ENR.decodeTxt(txt);
130+
EnrDecoder.fromString(txt);
130131
assert.fail("Expect error here");
131132
} catch (err: unknown) {
132133
const e = err as Error;
@@ -180,7 +181,7 @@ describe("ENR", function () {
180181
it("should return false", async () => {
181182
const txt =
182183
"enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg";
183-
const enr = await ENR.decodeTxt(txt);
184+
const enr = await EnrDecoder.fromString(txt);
184185
// should have id and public key inside ENR
185186
expect(enr.verify(new Uint8Array(32), new Uint8Array(64))).to.be.false;
186187
});
@@ -209,7 +210,7 @@ describe("ENR", function () {
209210

210211
it("should encode/decode to RLP encoding", async function () {
211212
const encoded = await record.encode(privateKey);
212-
const decoded = await ENR.decode(encoded);
213+
const decoded = await EnrDecoder.fromRLP(encoded);
213214

214215
record.forEach((value, key) => {
215216
expect(equals(decoded.get(key)!, value)).to.be.true;
@@ -220,7 +221,7 @@ describe("ENR", function () {
220221
// spec enr https://eips.ethereum.org/EIPS/eip-778
221222
const testTxt =
222223
"enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
223-
const decoded = await ENR.decodeTxt(testTxt);
224+
const decoded = await EnrDecoder.fromString(testTxt);
224225
// Note: Signatures are different due to the extra entropy added
225226
// by @noble/secp256k1:
226227
// https://github.com/paulmillr/noble-secp256k1#signmsghash-privatekey
@@ -403,7 +404,7 @@ describe("ENR", function () {
403404
enr.waku2 = waku2Protocols;
404405

405406
const txt = await enr.encodeTxt(privateKey);
406-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
407+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
407408

408409
expect(decoded.relay).to.equal(false);
409410
expect(decoded.store).to.equal(false);
@@ -419,7 +420,7 @@ describe("ENR", function () {
419420

420421
enr.waku2 = waku2Protocols;
421422
const txt = await enr.encodeTxt(privateKey);
422-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
423+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
423424

424425
expect(decoded.relay).to.equal(true);
425426
expect(decoded.store).to.equal(true);
@@ -432,7 +433,7 @@ describe("ENR", function () {
432433

433434
enr.waku2 = waku2Protocols;
434435
const txt = await enr.encodeTxt(privateKey);
435-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
436+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
436437

437438
expect(decoded.relay).to.equal(true);
438439
expect(decoded.store).to.equal(false);
@@ -445,7 +446,7 @@ describe("ENR", function () {
445446

446447
enr.waku2 = waku2Protocols;
447448
const txt = await enr.encodeTxt(privateKey);
448-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
449+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
449450

450451
expect(decoded.relay).to.equal(false);
451452
expect(decoded.store).to.equal(true);
@@ -458,7 +459,7 @@ describe("ENR", function () {
458459

459460
enr.waku2 = waku2Protocols;
460461
const txt = await enr.encodeTxt(privateKey);
461-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
462+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
462463

463464
expect(decoded.relay).to.equal(false);
464465
expect(decoded.store).to.equal(false);
@@ -471,7 +472,7 @@ describe("ENR", function () {
471472

472473
enr.waku2 = waku2Protocols;
473474
const txt = await enr.encodeTxt(privateKey);
474-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
475+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
475476

476477
expect(decoded.relay).to.equal(false);
477478
expect(decoded.store).to.equal(false);
@@ -485,7 +486,7 @@ describe("ENR", function () {
485486
const txt =
486487
"enr:-Iu4QADPfXNCM6iYyte0pIdbMirIw_AsKR7J1DeJBysXDWz4DZvyjgIwpMt-sXTVUzLJdE9FaStVy2ZKtHUVQAH61-KAgmlkgnY0gmlwhMCosvuJc2VjcDI1NmsxoQI0OCNtPJtBayNgvFvKp-0YyCozcvE1rqm_V1W51nHVv4N0Y3CC6mCFd2FrdTIH";
487488

488-
const decoded = (await ENR.decodeTxt(txt)).waku2!;
489+
const decoded = (await EnrDecoder.fromString(txt)).waku2!;
489490

490491
expect(decoded.relay).to.equal(true);
491492
expect(decoded.store).to.equal(true);

packages/enr/src/enr.ts

+1-53
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ import type {
1313
SequenceNumber,
1414
Waku2,
1515
} from "@waku/interfaces";
16-
import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils";
16+
import { bytesToUtf8, hexToBytes, utf8ToBytes } from "@waku/utils";
1717
import debug from "debug";
18-
import { fromString } from "uint8arrays/from-string";
1918
import { toString } from "uint8arrays/to-string";
2019

2120
import {
@@ -65,57 +64,6 @@ export class ENR extends Map<ENRKey, ENRValue> implements IEnr {
6564

6665
return enr;
6766
}
68-
69-
static async decodeFromValues(decoded: Uint8Array[]): Promise<ENR> {
70-
if (!Array.isArray(decoded)) {
71-
throw new Error("Decoded ENR must be an array");
72-
}
73-
if (decoded.length % 2 !== 0) {
74-
throw new Error("Decoded ENR must have an even number of elements");
75-
}
76-
const [signature, seq, ...kvs] = decoded;
77-
if (!signature || Array.isArray(signature)) {
78-
throw new Error("Decoded ENR invalid signature: must be a byte array");
79-
}
80-
if (!seq || Array.isArray(seq)) {
81-
throw new Error(
82-
"Decoded ENR invalid sequence number: must be a byte array"
83-
);
84-
}
85-
const obj: Record<ENRKey, ENRValue> = {};
86-
for (let i = 0; i < kvs.length; i += 2) {
87-
try {
88-
obj[bytesToUtf8(kvs[i])] = kvs[i + 1];
89-
} catch (e) {
90-
log("Failed to decode ENR key to UTF-8, skipping it", kvs[i], e);
91-
}
92-
}
93-
// If seq is an empty array, translate as value 0
94-
const hexSeq = "0x" + (seq.length ? bytesToHex(seq) : "00");
95-
96-
const enr = await ENR.create(obj, BigInt(hexSeq), signature);
97-
98-
const rlpEncodedBytes = hexToBytes(RLP.encode([seq, ...kvs]));
99-
if (!enr.verify(rlpEncodedBytes, signature)) {
100-
throw new Error("Unable to verify ENR signature");
101-
}
102-
return enr;
103-
}
104-
105-
static decode(encoded: Uint8Array): Promise<ENR> {
106-
const decoded = RLP.decode(encoded).map(hexToBytes);
107-
return ENR.decodeFromValues(decoded);
108-
}
109-
110-
static decodeTxt(encoded: string): Promise<ENR> {
111-
if (!encoded.startsWith(this.RECORD_PREFIX)) {
112-
throw new Error(
113-
`"string encoded ENR must start with '${this.RECORD_PREFIX}'`
114-
);
115-
}
116-
return ENR.decode(fromString(encoded.slice(4), "base64url"));
117-
}
118-
11967
set(k: ENRKey, v: ENRValue): this {
12068
this.signature = undefined;
12169
this.seq++;

packages/enr/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./constants.js";
22
export * from "./creator.js";
3+
export * from "./decoder.js";
34
export * from "./enr.js";
45
export * from "./peer_id.js";
56
export * from "./waku2_codec.js";

packages/peer-exchange/src/waku_peer_exchange.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
Registrar,
66
} from "@libp2p/interface-registrar";
77
import { BaseProtocol } from "@waku/core/lib/base_protocol";
8-
import { ENR } from "@waku/enr";
8+
import { EnrDecoder } from "@waku/enr";
99
import type {
1010
IPeerExchange,
1111
PeerExchangeQueryParams,
@@ -95,7 +95,7 @@ export class WakuPeerExchange extends BaseProtocol implements IPeerExchange {
9595

9696
const enrs = await Promise.all(
9797
decoded.peerInfos.map(
98-
(peerInfo) => peerInfo.enr && ENR.decode(peerInfo.enr)
98+
(peerInfo) => peerInfo.enr && EnrDecoder.fromRLP(peerInfo.enr)
9999
)
100100
);
101101

packages/tests/tests/enr.node.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { waitForRemotePeer } from "@waku/core";
22
import { createRelayNode } from "@waku/create";
3-
import { ENR } from "@waku/enr";
3+
import { EnrDecoder } from "@waku/enr";
44
import type { RelayNode } from "@waku/interfaces";
55
import { Protocols } from "@waku/interfaces";
66
import { expect } from "chai";
@@ -38,7 +38,7 @@ describe("ENR Interop: nwaku", function () {
3838
const nimPeerId = await nwaku.getPeerId();
3939

4040
expect(nwakuInfo.enrUri).to.not.be.undefined;
41-
const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? "");
41+
const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? "");
4242
expect(dec.peerId?.toString()).to.eq(nimPeerId.toString());
4343
expect(dec.waku2).to.deep.eq({
4444
relay: true,
@@ -70,7 +70,7 @@ describe("ENR Interop: nwaku", function () {
7070
const nimPeerId = await nwaku.getPeerId();
7171

7272
expect(nwakuInfo.enrUri).to.not.be.undefined;
73-
const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? "");
73+
const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? "");
7474
expect(dec.peerId?.toString()).to.eq(nimPeerId.toString());
7575
expect(dec.waku2).to.deep.eq({
7676
relay: true,
@@ -102,7 +102,7 @@ describe("ENR Interop: nwaku", function () {
102102
const nimPeerId = await nwaku.getPeerId();
103103

104104
expect(nwakuInfo.enrUri).to.not.be.undefined;
105-
const dec = await ENR.decodeTxt(nwakuInfo.enrUri ?? "");
105+
const dec = await EnrDecoder.fromString(nwakuInfo.enrUri ?? "");
106106
expect(dec.peerId?.toString()).to.eq(nimPeerId.toString());
107107
expect(dec.waku2).to.deep.eq({
108108
relay: true,

0 commit comments

Comments
 (0)