Merge pull request #1216 from waku-org/chore/libp2p-crypto-enr

This commit is contained in:
fryorcraken.eth 2023-03-10 11:02:57 +11:00 committed by GitHub
commit 36534af0e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 672 additions and 721 deletions

10
package-lock.json generated
View File

@ -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",

View File

@ -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;
}

View File

@ -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"),

View File

@ -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,27 +93,28 @@ 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) => {
if (
(await this._components.peerStore.getTags(peerInfo.id)).find(
({ name }) => name === DEFAULT_BOOTSTRAP_TAG_NAME
)
)
return;
await this._components.peerStore.tagPeer(
peerInfo.id,
DEFAULT_BOOTSTRAP_TAG_NAME,
{
value: this._options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
ttl: this._options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL,
}
);
this.dispatchEvent(
new CustomEvent<PeerInfo>("peer", { detail: peerInfo })
);
});
const peerInfo = peer.peerInfo;
if (!peerInfo) continue;
if (
(await this._components.peerStore.getTags(peerInfo.id)).find(
({ name }) => name === DEFAULT_BOOTSTRAP_TAG_NAME
)
)
continue;
await this._components.peerStore.tagPeer(
peerInfo.id,
DEFAULT_BOOTSTRAP_TAG_NAME,
{
value: this._options.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
ttl: this._options.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL,
}
);
this.dispatchEvent(
new CustomEvent<PeerInfo>("peer", { detail: peerInfo })
);
}
}

View File

@ -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",

View File

@ -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();
}
}
}

View File

@ -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.
*

View File

@ -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");
}
}

View File

@ -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")
);
}
}

View File

@ -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);

View File

@ -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;
export enum TransportProtocol {
TCP = "tcp",
UDP = "udp",
}
export enum TransportProtocolPerIpVersion {
TCP4 = "tcp4",
UDP4 = "udp4",
TCP6 = "tcp6",
UDP6 = "udp6",
}
private constructor(
kvs: Record<ENRKey, ENRValue> = {},
seq: SequenceNumber = BigInt(1),
signature?: Uint8Array
) {
super(Object.entries(kvs));
this.seq = seq;
this.signature = signature;
}
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")
);
}
}

View File

@ -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
);
}

View File

@ -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";

View File

@ -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()
);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}

212
packages/enr/src/raw_enr.ts Normal file
View File

@ -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]));
}
}
}

View File

@ -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

View File

@ -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",

View File

@ -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[];
}

View File

@ -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)
)
);

View File

@ -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,
})
);
}

View File

@ -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,

View File

@ -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",

View File

@ -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 {