mirror of https://github.com/waku-org/js-waku.git
chore!: extract decoder code
Separation of concerns by moving decoding logic in new class.
This commit is contained in:
parent
8fd1455122
commit
130c49b636
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<ENR> {
|
||||
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<ENR> {
|
||||
const decoded = RLP.decode(encoded).map(hexToBytes);
|
||||
return EnrDecoder.fromValues(decoded);
|
||||
}
|
||||
|
||||
private static async fromValues(decoded: Uint8Array[]): Promise<ENR> {
|
||||
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<ENRKey, ENRValue> = {};
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<ENRKey, ENRValue> implements IEnr {
|
|||
|
||||
return enr;
|
||||
}
|
||||
|
||||
static async decodeFromValues(decoded: Uint8Array[]): Promise<ENR> {
|
||||
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<ENRKey, ENRValue> = {};
|
||||
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<ENR> {
|
||||
const decoded = RLP.decode(encoded).map(hexToBytes);
|
||||
return ENR.decodeFromValues(decoded);
|
||||
}
|
||||
|
||||
static decodeTxt(encoded: string): Promise<ENR> {
|
||||
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++;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue