mirror of https://github.com/waku-org/js-waku.git
Merge pull request #1216 from waku-org/chore/libp2p-crypto-enr
This commit is contained in:
commit
36534af0e6
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<ENR> {
|
||||
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"),
|
||||
|
|
|
@ -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,14 +93,16 @@ 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) => {
|
||||
|
||||
const peerInfo = peer.peerInfo;
|
||||
if (!peerInfo) continue;
|
||||
|
||||
if (
|
||||
(await this._components.peerStore.getTags(peerInfo.id)).find(
|
||||
({ name }) => name === DEFAULT_BOOTSTRAP_TAG_NAME
|
||||
)
|
||||
)
|
||||
return;
|
||||
continue;
|
||||
|
||||
await this._components.peerStore.tagPeer(
|
||||
peerInfo.id,
|
||||
|
@ -114,7 +115,6 @@ export class PeerDiscoveryDns
|
|||
this.dispatchEvent(
|
||||
new CustomEvent<PeerInfo>("peer", { detail: peerInfo })
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ENRKey, ENRValue> = {}
|
||||
): Promise<ENR> {
|
||||
// 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<ENRKey, ENRValue> = {}
|
||||
): Promise<ENR> {
|
||||
switch (peerId.type) {
|
||||
case "secp256k1":
|
||||
return EnrCreator.fromPublicKey(getPublicKeyFromPeerId(peerId), kvs);
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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<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 fromValues(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
async function fromValues(values: Uint8Array[]): Promise<ENR> {
|
||||
const { signature, seq, kvs } = checkValues(values);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -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<ENRKey | ENRValue | number[]> = 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<Uint8Array> {
|
||||
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<string> {
|
||||
return (
|
||||
ENR.RECORD_PREFIX +
|
||||
toString(await EnrEncoder.toBytes(enr, privateKey), "base64url")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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<ENRKey, ENRValue> implements IEnr {
|
||||
public static readonly RECORD_PREFIX = "enr:";
|
||||
public seq: SequenceNumber;
|
||||
public signature?: Uint8Array;
|
||||
public peerId?: PeerId;
|
||||
|
||||
private constructor(
|
||||
kvs: Record<ENRKey, ENRValue> = {},
|
||||
seq: SequenceNumber = BigInt(1),
|
||||
signature?: Uint8Array
|
||||
) {
|
||||
super(Object.entries(kvs));
|
||||
this.seq = seq;
|
||||
this.signature = signature;
|
||||
export enum TransportProtocol {
|
||||
TCP = "tcp",
|
||||
UDP = "udp",
|
||||
}
|
||||
export enum TransportProtocolPerIpVersion {
|
||||
TCP4 = "tcp4",
|
||||
UDP4 = "udp4",
|
||||
TCP6 = "tcp6",
|
||||
UDP6 = "udp6",
|
||||
}
|
||||
|
||||
export class ENR extends RawEnr implements IEnr {
|
||||
public static readonly RECORD_PREFIX = "enr:";
|
||||
public peerId?: PeerId;
|
||||
|
||||
static async create(
|
||||
kvs: Record<ENRKey, ENRValue> = {},
|
||||
|
@ -63,8 +43,7 @@ export class ENR extends Map<ENRKey, ENRValue> 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<ENRKey, ENRValue> implements IEnr {
|
|||
return enr;
|
||||
}
|
||||
|
||||
static createV4(
|
||||
publicKey: Uint8Array,
|
||||
kvs: Record<ENRKey, ENRValue> = {}
|
||||
): Promise<ENR> {
|
||||
// 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<ENRKey, ENRValue> = {}
|
||||
): Promise<ENR> {
|
||||
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<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++;
|
||||
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<ENRKey, ENRValue> 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<ENRKey, ENRValue> 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<ENRKey, ENRValue> 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<ENRKey, ENRValue> 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<ENRKey, ENRValue> 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<ENRKey | ENRValue | number[]> = 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<Uint8Array> {
|
||||
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<string> {
|
||||
return (
|
||||
ENR.RECORD_PREFIX + toString(await this.encode(privateKey), "base64url")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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<PeerId> {
|
||||
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<IKeypair> {
|
||||
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()
|
||||
);
|
||||
}
|
|
@ -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<Secp256k1Keypair> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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<PeerId> {
|
||||
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<Uint8Array> {
|
||||
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();
|
||||
}
|
|
@ -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<ENRKey, ENRValue> {
|
||||
public seq: SequenceNumber;
|
||||
public signature?: Uint8Array;
|
||||
|
||||
protected constructor(
|
||||
kvs: Record<ENRKey, ENRValue> = {},
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<ENRKey, ENRValue> {
|
|||
udp6?: number;
|
||||
multiaddrs?: Multiaddr[];
|
||||
waku2?: Waku2;
|
||||
peerInfo: PeerInfo | undefined;
|
||||
|
||||
encode(privateKey?: Uint8Array): Promise<Uint8Array>;
|
||||
/**
|
||||
* @deprecated: use { @link IEnr.peerInfo } instead.
|
||||
*/
|
||||
getFullMultiaddrs(): Multiaddr[];
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
@ -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<PeerDiscoveryEvents>
|
||||
|
@ -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<PeerInfo>("peer", {
|
||||
detail: {
|
||||
id: peerId,
|
||||
multiaddrs,
|
||||
protocols: [],
|
||||
},
|
||||
detail: peerInfo,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue