diff --git a/src/enr/constants.ts b/src/enr/constants.ts index 7786429e..07fa3abf 100644 --- a/src/enr/constants.ts +++ b/src/enr/constants.ts @@ -2,5 +2,3 @@ export const MAX_RECORD_SIZE = 300; export const ERR_INVALID_ID = "Invalid record id"; - -export const ERR_NO_SIGNATURE = "No valid signature found"; diff --git a/src/enr/enr.ts b/src/enr/enr.ts index 46304eec..562e1ffe 100644 --- a/src/enr/enr.ts +++ b/src/enr/enr.ts @@ -6,7 +6,7 @@ import { PeerId } from "@libp2p/interface-peer-id"; import { convertToString, convertToBytes } from "@multiformats/multiaddr/convert"; import { encode as varintEncode } from "varint"; -import { ERR_INVALID_ID, ERR_NO_SIGNATURE, MAX_RECORD_SIZE } from "./constants.js"; +import { ERR_INVALID_ID, MAX_RECORD_SIZE } from "./constants.js"; import * as v4 from "./v4.js"; import { ENRKey, ENRValue, SequenceNumber, NodeId } from "./types.js"; import { @@ -18,136 +18,477 @@ import { } from "../keypair/index.js"; import { toNewUint8Array } from "../util/index.js"; -export class ENR extends Map { - public seq: SequenceNumber; - public signature: Buffer | null; - private _nodeId?: NodeId; +/** ENR identity scheme */ +export enum IDScheme { + v4 = "v4", +} - constructor(kvs: Record = {}, seq: SequenceNumber = 1n, signature: Buffer | null = null) { - super(Object.entries(kvs)); - this.seq = seq; - this.signature = signature; - } - static createV4(publicKey: Buffer, kvs: Record = {}): ENR { - return new ENR({ - ...kvs, - id: Buffer.from("v4"), - secp256k1: publicKey, - }); +/** Raw data included in an ENR */ +export type ENRData = { + kvs: ReadonlyMap; + seq: SequenceNumber; + signature: Uint8Array; +}; + +/** Raw data included in a read+write ENR */ +export type SignableENRData = { + kvs: ReadonlyMap; + seq: SequenceNumber; + privateKey: Uint8Array; +}; + +export function id(kvs: ReadonlyMap): IDScheme { + const idBuf = kvs.get("id"); + if (!idBuf) throw new Error("id not found"); + const id = Buffer.from(idBuf).toString("utf8") as IDScheme; + if (IDScheme[id] == null) { + throw new Error("Unknown enr id scheme: " + id); + } + return id; +} + +export function nodeId(id: IDScheme, publicKey: Buffer): NodeId { + switch (id) { + case IDScheme.v4: + return v4.nodeId(publicKey); + default: + throw new Error(ERR_INVALID_ID); } - static createFromPeerId(peerId: PeerId, kvs: Record = {}): ENR { - const keypair = createKeypairFromPeerId(peerId); - switch (keypair.type) { - case KeypairType.Secp256k1: - return ENR.createV4(keypair.publicKey, kvs); - default: - throw new Error(); +} +export function publicKey(id: IDScheme, kvs: ReadonlyMap): Uint8Array { + switch (id) { + case IDScheme.v4: { + const pubkey = kvs.get("secp256k1"); + if (!pubkey) { + throw new Error("Pubkey doesn't exist"); + } + return pubkey; } + default: + throw new Error(ERR_INVALID_ID); } - static decodeFromValues(decoded: Buffer[]): ENR { - if (!Array.isArray(decoded)) { - throw new Error("Decoded ENR must be an array"); +} +export function keypairType(id: IDScheme): KeypairType { + switch (id) { + case "v4": + return KeypairType.Secp256k1; + default: + throw new Error(ERR_INVALID_ID); + } +} + +export function verify(id: IDScheme, data: Uint8Array, publicKey: Buffer, signature: Uint8Array): boolean { + switch (id) { + case IDScheme.v4: + return v4.verify(publicKey, Buffer.from(data), Buffer.from(signature)); + default: + throw new Error(ERR_INVALID_ID); + } +} +export function sign(id: IDScheme, data: Uint8Array, privateKey: Buffer): Buffer { + switch (id) { + case IDScheme.v4: + return v4.sign(privateKey, Buffer.from(data)); + default: + throw new Error(ERR_INVALID_ID); + } +} + +export function encodeToValues( + kvs: ReadonlyMap, + seq: SequenceNumber, + signature?: Uint8Array +): (ENRKey | ENRValue | number)[] { + // sort keys and flatten into [k, v, k, v, ...] + const content: Array = Array.from(kvs.keys()) + .sort((a, b) => a.localeCompare(b)) + .map((k) => [k, kvs.get(k)] as [ENRKey, ENRValue]) + .flat(); + content.unshift(Number(seq)); + if (signature) { + content.unshift(signature); + } + return content; +} + +export function encode(kvs: ReadonlyMap, seq: SequenceNumber, signature: Uint8Array): Uint8Array { + const encoded = RLP.encode(encodeToValues(kvs, seq, signature)); + if (encoded.length >= MAX_RECORD_SIZE) { + throw new Error("ENR must be less than 300 bytes"); + } + return encoded; +} + +export function decodeFromValues(decoded: Uint8Array[]): ENRData { + 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] = 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 kvs = new Map(); + const signed: Uint8Array[] = [seq]; + for (let i = 2; i < decoded.length; i += 2) { + const k = decoded[i]; + const v = decoded[i + 1]; + kvs.set(k.toString(), v); + signed.push(k, v); + } + const _id = id(kvs); + if (!verify(_id, RLP.encode(signed), Buffer.from(publicKey(_id, kvs)), signature)) { + throw new Error("Unable to verify enr signature"); + } + return { + kvs, + seq: toBigIntBE(Buffer.from(seq)), + signature, + }; +} +export function decode(encoded: Uint8Array): ENRData { + return decodeFromValues(RLP.decode(encoded) as Uint8Array[]); +} +export function txtToBuf(encoded: string): Uint8Array { + if (!encoded.startsWith("enr:")) { + throw new Error("string encoded ENR must start with 'enr:'"); + } + return base64url.toBuffer(encoded.slice(4)); +} +export function decodeTxt(encoded: string): ENRData { + return decode(txtToBuf(encoded)); +} + +// IP / Protocol + +export type Protocol = "udp" | "tcp" | "udp4" | "udp6" | "tcp4" | "tcp6"; + +export function getIPValue(kvs: ReadonlyMap, key: string, multifmtStr: string): string | undefined { + const raw = kvs.get(key); + if (raw) { + return convertToString(multifmtStr, toNewUint8Array(raw)) as string; + } else { + return undefined; + } +} + +export function getProtocolValue(kvs: ReadonlyMap, key: string): number | undefined { + const raw = kvs.get(key); + if (raw) { + const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); + return view.getUint16(0); + } else { + return undefined; + } +} + +export function portToBuf(port: number): Uint8Array { + const buf = new Uint8Array(2); + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + view.setUint16(0, port); + return buf; +} + +// Classes + +export abstract class BaseENR { + /** Raw enr key-values */ + public abstract kvs: ReadonlyMap; + /** Sequence number */ + public abstract seq: SequenceNumber; + public abstract signature: Uint8Array; + + // Identity methods + + /** Node identifier */ + public abstract nodeId: NodeId; + public abstract publicKey: Uint8Array; + public abstract keypair: IKeypair; + + /** enr identity scheme */ + get id(): IDScheme { + return id(this.kvs); + } + get keypairType(): KeypairType { + return keypairType(this.id); + } + async peerId(): Promise { + return createPeerIdFromKeypair(this.keypair); + } + + // Network methods + + get ip(): string | undefined { + return getIPValue(this.kvs, "ip", "ip4"); + } + get tcp(): number | undefined { + return getProtocolValue(this.kvs, "tcp"); + } + get udp(): number | undefined { + return getProtocolValue(this.kvs, "udp"); + } + get ip6(): string | undefined { + return getIPValue(this.kvs, "ip6", "ip6"); + } + get tcp6(): number | undefined { + return getProtocolValue(this.kvs, "tcp6"); + } + get udp6(): number | undefined { + return getProtocolValue(this.kvs, "udp6"); + } + getLocationMultiaddr(protocol: Protocol): Multiaddr | undefined { + if (protocol === "udp") { + return this.getLocationMultiaddr("udp4") || this.getLocationMultiaddr("udp6"); + } + if (protocol === "tcp") { + return this.getLocationMultiaddr("tcp4") || this.getLocationMultiaddr("tcp6"); } - if (decoded.length % 2 !== 0) { - throw new Error("Decoded ENR must have an even number of elements"); + const isIpv6 = protocol.endsWith("6"); + const ipVal = this.kvs.get(isIpv6 ? "ip6" : "ip"); + if (!ipVal) { + return undefined; } - const [signature, seq] = decoded; - if (!signature || Array.isArray(signature)) { - throw new Error("Decoded ENR invalid signature: must be a byte array"); + const isUdp = protocol.startsWith("udp"); + const isTcp = protocol.startsWith("tcp"); + let protoName, protoVal; + if (isUdp) { + protoName = "udp"; + protoVal = isIpv6 ? this.kvs.get("udp6") : this.kvs.get("udp"); + } else if (isTcp) { + protoName = "tcp"; + protoVal = isIpv6 ? this.kvs.get("tcp6") : this.kvs.get("tcp"); + } else { + return undefined; } - if (!seq || Array.isArray(seq)) { - throw new Error("Decoded ENR invalid sequence number: must be a byte array"); + if (!protoVal) { + return undefined; } - const obj: Record = {}; - const signed: Buffer[] = [seq]; - for (let i = 2; i < decoded.length; i += 2) { - const k = decoded[i]; - const v = decoded[i + 1]; - obj[k.toString()] = Buffer.from(v); - signed.push(k, v); - } - const enr = new ENR(obj, toBigIntBE(seq), signature); - if (!enr.verify(RLP.encode(signed), signature)) { - throw new Error("Unable to verify enr signature"); + // Create raw multiaddr buffer + // multiaddr length is: + // 1 byte for the ip protocol (ip4 or ip6) + // N bytes for the ip address + // 1 or 2 bytes for the protocol as buffer (tcp or udp) + // 2 bytes for the port + const ipMa = protocols(isIpv6 ? "ip6" : "ip4"); + const ipByteLen = ipMa.size / 8; + const protoMa = protocols(protoName); + const protoBuf = varintEncode(protoMa.code); + const maBuf = new Uint8Array(3 + ipByteLen + protoBuf.length); + maBuf[0] = ipMa.code; + maBuf.set(ipVal, 1); + maBuf.set(protoBuf, 1 + ipByteLen); + maBuf.set(protoVal, 1 + ipByteLen + protoBuf.length); + + return multiaddr(maBuf); + } + async getFullMultiaddr(protocol: Protocol): Promise { + const locationMultiaddr = this.getLocationMultiaddr(protocol); + if (locationMultiaddr) { + const peerId = await this.peerId(); + return locationMultiaddr.encapsulate(`/p2p/${peerId.toString()}`); } - return enr; } - static decode(encoded: Buffer): ENR { - const decoded = RLP.decode(encoded) as unknown as Buffer[]; - return ENR.decodeFromValues(decoded); + + // Serialization methods + + abstract encodeToValues(): (ENRKey | ENRValue | number)[]; + abstract encode(): Uint8Array; + encodeTxt(): string { + return "enr:" + base64url.encode(Buffer.from(this.encode())); + } +} +/** + * Ethereum Name Record + * + * https://eips.ethereum.org/EIPS/eip-778 + * + * `ENR` is used to read serialized ENRs and may not be modified once created. + */ +export class ENR extends BaseENR { + public kvs: ReadonlyMap; + public seq: SequenceNumber; + public signature: Uint8Array; + public nodeId: string; + private encoded?: Uint8Array; + + constructor( + kvs: ReadonlyMap | Record, + seq: SequenceNumber, + signature: Uint8Array, + encoded?: Uint8Array + ) { + super(); + this.kvs = new Map(kvs instanceof Map ? kvs.entries() : Object.entries(kvs)); + this.seq = seq; + this.signature = signature; + this.nodeId = nodeId(this.id, Buffer.from(this.publicKey)); + this.encoded = encoded; + } + + static fromObject(obj: ENRData): ENR { + return new ENR(obj.kvs, obj.seq, obj.signature); + } + static decodeFromValues(encoded: Uint8Array[]): ENR { + const { kvs, seq, signature } = decodeFromValues(encoded); + return new ENR(kvs, seq, signature); + } + static decode(encoded: Uint8Array): ENR { + const { kvs, seq, signature } = decode(encoded); + return new ENR(kvs, seq, signature, encoded); } static decodeTxt(encoded: string): ENR { - if (!encoded.startsWith("enr:")) { - throw new Error("string encoded ENR must start with 'enr:'"); + const encodedBuf = txtToBuf(encoded); + const { kvs, seq, signature } = decode(encodedBuf); + return new ENR(kvs, seq, signature, encodedBuf); + } + + get keypair(): IKeypair { + return createKeypair(this.keypairType, undefined, Buffer.from(this.publicKey)); + } + get publicKey(): Uint8Array { + return publicKey(this.id, this.kvs); + } + + toObject(): ENRData { + return { + kvs: this.kvs, + seq: this.seq, + signature: this.signature, + }; + } + + encodeToValues(): Uint8Array[] { + return RLP.decode(this.encode()) as Uint8Array[]; + } + encode(): Uint8Array { + if (!this.encoded) { + this.encoded = encode(this.kvs, this.seq, this.signature); } - return ENR.decode(base64url.toBuffer(encoded.slice(4))); + return this.encoded; } - set(k: ENRKey, v: ENRValue): this { - this.signature = null; - this.seq++; - if (k === "secp256k1" && this.id === "v4") { - delete this._nodeId; +} + +/** + * Ethereum Name Record + * + * https://eips.ethereum.org/EIPS/eip-778 + * + * `SignableENR` is used to create and update ENRs. + */ +export class SignableENR extends BaseENR { + public kvs: ReadonlyMap; + public seq: SequenceNumber; + public keypair: IKeypair; + public nodeId: NodeId; + private _signature?: Uint8Array; + + constructor( + kvs: ReadonlyMap | Record = {}, + seq: SequenceNumber = 1n, + keypair: IKeypair, + signature?: Uint8Array + ) { + super(); + this.kvs = new Map(kvs instanceof Map ? kvs.entries() : Object.entries(kvs)); + this.seq = seq; + this.keypair = keypair; + this.nodeId = nodeId(this.id, Buffer.from(this.publicKey)); + this._signature = signature; + + if (!this.keypair.publicKey.equals(publicKey(this.id, this.kvs))) { + throw new Error("Provided keypair doesn't match kv pubkey"); } - return super.set(k, v); } - delete(k: ENRKey): boolean { - this.signature = null; - this.seq++; - if (k === "secp256k1" && this.id === "v4") { - delete this._nodeId; + + static fromObject(obj: SignableENRData): SignableENR { + const _id = id(obj.kvs); + return new SignableENR( + obj.kvs, + obj.seq, + createKeypair(keypairType(_id), Buffer.from(obj.privateKey), Buffer.from(publicKey(_id, obj.kvs))) + ); + } + static createV4(keypair: IKeypair, kvs: Record = {}): SignableENR { + return new SignableENR( + { + ...kvs, + id: Buffer.from("v4"), + secp256k1: keypair.publicKey, + }, + BigInt(1), + keypair + ); + } + static createFromPeerId(peerId: PeerId, kvs: Record = {}): SignableENR { + const keypair = createKeypairFromPeerId(peerId); + switch (keypair.type) { + case KeypairType.Secp256k1: + return SignableENR.createV4(keypair, kvs); + default: + throw new Error(); } - return super.delete(k); } - get id(): string { - const id = this.get("id") as Buffer; - if (!id) throw new Error("id not found."); - return id.toString("utf8"); + static decodeFromValues(encoded: Uint8Array[], keypair: IKeypair): SignableENR { + const { kvs, seq, signature } = decodeFromValues(encoded); + return new SignableENR(kvs, seq, keypair, signature); } - get keypairType(): KeypairType { - switch (this.id) { - case "v4": - return KeypairType.Secp256k1; - default: - throw new Error(ERR_INVALID_ID); + static decode(encoded: Uint8Array, keypair: IKeypair): SignableENR { + const { kvs, seq, signature } = decode(encoded); + return new SignableENR(kvs, seq, keypair, signature); + } + static decodeTxt(encoded: string, keypair: IKeypair): SignableENR { + const { kvs, seq, signature } = decodeTxt(encoded); + return new SignableENR(kvs, seq, keypair, signature); + } + + get signature(): Uint8Array { + if (!this._signature) { + this._signature = sign(this.id, RLP.encode(encodeToValues(this.kvs, this.seq)), this.keypair.privateKey); } + return this._signature; } - get publicKey(): Buffer { - switch (this.id) { - case "v4": - return this.get("secp256k1") as Buffer; - default: - throw new Error(ERR_INVALID_ID); + set(k: ENRKey, v: ENRValue): void { + if (k === "secp256k1" && this.id === "v4") { + throw new Error("Unable to update `secp256k1` value"); } + // cache invalidation on any mutation + this._signature = undefined; + this.seq++; + (this.kvs as Map).set(k, v); } - get keypair(): IKeypair { - return createKeypair(this.keypairType, undefined, this.publicKey); + delete(k: ENRKey): boolean { + if (k === "secp256k1" && this.id === "v4") { + throw new Error("Unable to update `secp256k1` value"); + } + // cache invalidation on any mutation + this._signature = undefined; + this.seq++; + return (this.kvs as Map).delete(k); + } + + // Identity methods + + get publicKey(): Buffer { + return this.keypair.publicKey; } async peerId(): Promise { return createPeerIdFromKeypair(this.keypair); } - get nodeId(): NodeId { - if (this._nodeId) { - return this._nodeId; - } - switch (this.id) { - case "v4": - return (this._nodeId = v4.nodeId(this.publicKey)); - default: - throw new Error(ERR_INVALID_ID); - } - } + + // Network methods get ip(): string | undefined { - const raw = this.get("ip"); - if (raw) { - return convertToString("ip4", toNewUint8Array(raw)) as string; - } else { - return undefined; - } + return getIPValue(this.kvs, "ip", "ip4"); } - set ip(ip: string | undefined) { if (ip) { this.set("ip", convertToBytes("ip4", ip)); @@ -155,50 +496,29 @@ export class ENR extends Map { this.delete("ip"); } } - get tcp(): number | undefined { - const raw = this.get("tcp"); - if (raw) { - return Number(convertToString("tcp", toNewUint8Array(raw))); - } else { - return undefined; - } + return getProtocolValue(this.kvs, "tcp"); } - set tcp(port: number | undefined) { if (port === undefined) { this.delete("tcp"); } else { - this.set("tcp", convertToBytes("tcp", String(port))); + this.set("tcp", portToBuf(port)); } } - get udp(): number | undefined { - const raw = this.get("udp"); - if (raw) { - return Number(convertToString("udp", toNewUint8Array(raw))); - } else { - return undefined; - } + return getProtocolValue(this.kvs, "udp"); } - set udp(port: number | undefined) { if (port === undefined) { this.delete("udp"); } else { - this.set("udp", convertToBytes("udp", String(port))); + this.set("udp", portToBuf(port)); } } - get ip6(): string | undefined { - const raw = this.get("ip6"); - if (raw) { - return convertToString("ip6", toNewUint8Array(raw)) as string; - } else { - return undefined; - } + return getIPValue(this.kvs, "ip6", "ip6"); } - set ip6(ip: string | undefined) { if (ip) { this.set("ip6", convertToBytes("ip6", ip)); @@ -206,87 +526,25 @@ export class ENR extends Map { this.delete("ip6"); } } - get tcp6(): number | undefined { - const raw = this.get("tcp6"); - if (raw) { - return Number(convertToString("tcp", toNewUint8Array(raw))); - } else { - return undefined; - } + return getProtocolValue(this.kvs, "tcp6"); } - set tcp6(port: number | undefined) { if (port === undefined) { this.delete("tcp6"); } else { - this.set("tcp6", convertToBytes("tcp", String(port))); + this.set("tcp6", portToBuf(port)); } } - get udp6(): number | undefined { - const raw = this.get("udp6"); - if (raw) { - return Number(convertToString("udp", toNewUint8Array(raw))); - } else { - return undefined; - } + return getProtocolValue(this.kvs, "udp6"); } - set udp6(port: number | undefined) { if (port === undefined) { this.delete("udp6"); } else { - this.set("udp6", convertToBytes("udp", String(port))); - } - } - - 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 undefined; - } - - 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 undefined; + this.set("udp6", portToBuf(port)); } - if (!protoVal) { - return undefined; - } - - // Create raw multiaddr buffer - // multiaddr length is: - // 1 byte for the ip protocol (ip4 or ip6) - // N bytes for the ip address - // 1 or 2 bytes for the protocol as buffer (tcp or udp) - // 2 bytes for the port - const ipMa = protocols(isIpv6 ? "ip6" : "ip4"); - const ipByteLen = ipMa.size / 8; - const protoMa = protocols(protoName); - const protoBuf = varintEncode(protoMa.code); - const maBuf = new Uint8Array(3 + ipByteLen + protoBuf.length); - maBuf[0] = ipMa.code; - maBuf.set(ipVal, 1); - maBuf.set(protoBuf, 1 + ipByteLen); - maBuf.set(protoVal, 1 + ipByteLen + protoBuf.length); - - return multiaddr(maBuf); } setLocationMultiaddr(multiaddr: Multiaddr): void { const protoNames = multiaddr.protoNames(); @@ -307,57 +565,22 @@ export class ENR extends Map { this.set(protoNames[1] + "6", tuples[1][1]); } } - async getFullMultiaddr(protocol: "udp" | "udp4" | "udp6" | "tcp" | "tcp4" | "tcp6"): Promise { - const locationMultiaddr = this.getLocationMultiaddr(protocol); - if (locationMultiaddr) { - const peerId = await this.peerId(); - return locationMultiaddr.encapsulate(`/p2p/${peerId.toString()}`); - } - } - verify(data: Buffer, signature: Buffer): boolean { - if (!this.get("id") || this.id !== "v4") { - throw new Error(ERR_INVALID_ID); - } - if (!this.publicKey) { - throw new Error("Failed to verify enr: No public key"); - } - return v4.verify(this.publicKey, data, signature); + + toObject(): SignableENRData { + return { + kvs: this.kvs, + seq: this.seq, + privateKey: new Uint8Array(this.keypair.privateKey), + }; } - sign(data: Buffer, privateKey: Buffer): Buffer { - switch (this.id) { - case "v4": - this.signature = v4.sign(privateKey, data); - break; - default: - throw new Error(ERR_INVALID_ID); - } - return this.signature; - } - encodeToValues(privateKey?: Buffer): (ENRKey | ENRValue | number)[] { - // sort keys and flatten into [k, v, k, v, ...] - const content: Array = Array.from(this.keys()) - .sort((a, b) => a.localeCompare(b)) - .map((k) => [k, this.get(k)] as [ENRKey, ENRValue]) - .flat(); - content.unshift(Number(this.seq)); - if (privateKey) { - content.unshift(this.sign(RLP.encode(content), privateKey)); - } else { - if (!this.signature) { - throw new Error(ERR_NO_SIGNATURE); - } - content.unshift(this.signature); - } - return content; + encodeToValues(): (string | number | Uint8Array)[] { + return encodeToValues(this.kvs, this.seq, this.signature); } - encode(privateKey?: Buffer): Buffer { - const encoded = RLP.encode(this.encodeToValues(privateKey)); - if (encoded.length >= MAX_RECORD_SIZE) { - throw new Error("ENR must be less than 300 bytes"); - } - return encoded; + encode(): Uint8Array { + return encode(this.kvs, this.seq, this.signature); } - encodeTxt(privateKey?: Buffer): string { - return "enr:" + base64url.encode(Buffer.from(this.encode(privateKey))); + + toENR(): ENR { + return new ENR(this.kvs, this.seq, this.signature, this.encode()); } } diff --git a/src/libp2p/discv5.ts b/src/libp2p/discv5.ts index 42186d4f..1bf2954c 100644 --- a/src/libp2p/discv5.ts +++ b/src/libp2p/discv5.ts @@ -4,7 +4,7 @@ import { PeerInfo } from "@libp2p/interface-peer-info"; import { CustomEvent, EventEmitter } from "@libp2p/interfaces/events"; import { multiaddr } from "@multiformats/multiaddr"; -import { Discv5, ENRInput } from "../service/index.js"; +import { Discv5, ENRInput, SignableENRInput } from "../service/index.js"; import { ENR } from "../enr/index.js"; import { IDiscv5Config } from "../config/index.js"; import { MetricsRegister } from "../metrics.js"; @@ -18,7 +18,7 @@ export interface IDiscv5DiscoveryInputOptions extends Partial { /** * Local ENR associated with the local libp2p peer id */ - enr: ENRInput; + enr: SignableENRInput; /** * The bind multiaddr for the discv5 UDP server * diff --git a/src/packet/types.ts b/src/packet/types.ts index 0dd1e10e..36c5fd97 100644 --- a/src/packet/types.ts +++ b/src/packet/types.ts @@ -74,7 +74,7 @@ export interface IHandshakeAuthdata { idSignature: Buffer; ephPubkey: Buffer; // pre-encoded ENR - record?: Buffer; + record?: Uint8Array; } export interface IPacket { diff --git a/src/service/service.ts b/src/service/service.ts index ce04ae99..a69eb8d0 100644 --- a/src/service/service.ts +++ b/src/service/service.ts @@ -7,7 +7,7 @@ import { PeerId } from "@libp2p/interface-peer-id"; import { ITransportService, UDPTransportService } from "../transport/index.js"; import { MAX_PACKET_SIZE } from "../packet/index.js"; import { ConnectionDirection, RequestErrorType, SessionService } from "../session/index.js"; -import { ENR, NodeId, MAX_RECORD_SIZE, createNodeId } from "../enr/index.js"; +import { ENR, NodeId, MAX_RECORD_SIZE, createNodeId, SignableENR } from "../enr/index.js"; import { IKeypair, createKeypairFromPeerId, createPeerIdFromKeypair } from "../keypair/index.js"; import { EntryStatus, @@ -48,6 +48,7 @@ import { IActiveRequest, INodesResponse, PongResponse, + SignableENRInput, } from "./types.js"; import { RateLimiter, RateLimiterOpts } from "../rateLimit/index.js"; import { @@ -77,7 +78,7 @@ const log = debug("discv5:service"); */ export interface IDiscv5CreateOptions { - enr: ENRInput; + enr: SignableENRInput; peerId: PeerId; multiaddr: Multiaddr; config?: Partial; @@ -200,12 +201,13 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { const { enr, peerId, multiaddr, config = {}, metricsRegistry, transport } = opts; const fullConfig = { ...defaultConfig, ...config }; const metrics = metricsRegistry ? createDiscv5Metrics(metricsRegistry) : undefined; - const decodedEnr = typeof enr === "string" ? ENR.decodeTxt(enr) : enr; + const keypair = createKeypairFromPeerId(peerId); + const decodedEnr = typeof enr === "string" ? SignableENR.decodeTxt(enr, keypair) : enr; const rateLimiter = opts.rateLimiterOpts && new RateLimiter(opts.rateLimiterOpts, metrics ?? null); const sessionService = new SessionService( fullConfig, decodedEnr, - createKeypairFromPeerId(peerId), + keypair, transport ?? new UDPTransportService(multiaddr, decodedEnr.nodeId, rateLimiter) ); return new Discv5(fullConfig, sessionService, metrics); @@ -297,7 +299,7 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { return createPeerIdFromKeypair(this.keypair); } - public get enr(): ENR { + public get enr(): SignableENR { return this.sessionService.enr; } @@ -791,11 +793,7 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { } // if the distance is 0, send our local ENR if (distance === 0) { - // ensure our enr has signature before sending response - if (!this.enr.signature) { - this.enr.encodeToValues(this.keypair.privateKey); - } - nodes.push(this.enr); + nodes.push(this.enr.toENR()); } else { nodes.push(...this.kbuckets.valuesOfDistance(distance)); } diff --git a/src/service/types.ts b/src/service/types.ts index 53aee9dd..9dd7964a 100644 --- a/src/service/types.ts +++ b/src/service/types.ts @@ -2,7 +2,7 @@ import { EventEmitter } from "events"; import StrictEventEmitter from "strict-event-emitter-types"; import { Multiaddr } from "@multiformats/multiaddr"; -import { ENR, SequenceNumber } from "../enr/index.js"; +import { ENR, SequenceNumber, SignableENR } from "../enr/index.js"; import { ITalkReqMessage, ITalkRespMessage, RequestMessage } from "../message/index.js"; import { INodeAddress, NodeContact } from "../session/nodeInfo.js"; import { ConnectionDirection, RequestErrorType } from "../session/index.js"; @@ -108,3 +108,4 @@ export type ConnectionStatus = }; export type ENRInput = ENR | string; +export type SignableENRInput = SignableENR | string; diff --git a/src/session/service.ts b/src/session/service.ts index 9befbb19..59c0ad79 100644 --- a/src/session/service.ts +++ b/src/session/service.ts @@ -16,7 +16,7 @@ import { createRandomPacket, createWhoAreYouPacket, } from "../packet/index.js"; -import { ENR } from "../enr/index.js"; +import { ENR, SignableENR } from "../enr/index.js"; import { ERR_INVALID_SIG, Session } from "./session.js"; import { IKeypair } from "../keypair/index.js"; import { @@ -73,7 +73,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte /** * The local ENR */ - public enr: ENR; + public enr: SignableENR; /** * The keypair to sign the ENR and set up encrypted communication with peers */ @@ -134,7 +134,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte */ private sessions: LRUCache; - constructor(config: ISessionConfig, enr: ENR, keypair: IKeypair, transport: ITransportService) { + constructor(config: ISessionConfig, enr: SignableENR, keypair: IKeypair, transport: ITransportService) { super(); // ensure the keypair matches the one that signed the ENR @@ -391,7 +391,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte // Encrypt the message with an auth header and respond // First if a new version of our ENR is requested, obtain it for the header - const updatedEnr = authdata.enrSeq < this.enr.seq ? this.enr.encode(this.keypair.privateKey) : null; + const updatedEnr = authdata.enrSeq < this.enr.seq ? this.enr.encode() : null; // Generate a new session and authentication packet const [authPacket, session] = Session.encryptWithHeader( diff --git a/src/session/session.ts b/src/session/session.ts index 22e33e99..520f7c63 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -79,7 +79,7 @@ export class Session { challenge: IChallenge, idSignature: Buffer, ephPubkey: Buffer, - enrRecord?: Buffer + enrRecord?: Uint8Array ): [Session, ENR] { let enr: ENR; // check and verify a potential ENR update @@ -122,7 +122,7 @@ export class Session { remoteContact: NodeContact, localKey: IKeypair, localNodeId: NodeId, - updatedEnr: Buffer | null, + updatedEnr: Uint8Array | null, challengeData: Buffer, message: Buffer ): [IPacket, Session] { diff --git a/src/util/ip.ts b/src/util/ip.ts index 242b7781..5a26cb14 100644 --- a/src/util/ip.ts +++ b/src/util/ip.ts @@ -1,5 +1,5 @@ import { multiaddr, Multiaddr } from "@multiformats/multiaddr"; -import { ENR } from "../enr/index.js"; +import { BaseENR, SignableENR } from "../enr/index.js"; export type IP = { type: 4 | 6; octets: Uint8Array }; export type SocketAddress = { @@ -35,8 +35,8 @@ export function isEqualSocketAddress(s1: SocketAddress, s2: SocketAddress): bool return s1.port === s2.port; } -export function getSocketAddressOnENR(enr: ENR): SocketAddress | undefined { - const ip4Octets = enr.get("ip"); +export function getSocketAddressOnENR(enr: BaseENR): SocketAddress | undefined { + const ip4Octets = enr.kvs.get("ip"); const udp4 = enr.udp; if (ip4Octets !== undefined && udp4 !== undefined) { const ip = ipFromBytes(ip4Octets); @@ -48,7 +48,7 @@ export function getSocketAddressOnENR(enr: ENR): SocketAddress | undefined { } } - const ip6Octets = enr.get("ip6"); + const ip6Octets = enr.kvs.get("ip6"); const udp6 = enr.udp6; if (ip6Octets !== undefined && udp6 !== undefined) { const ip = ipFromBytes(ip6Octets); @@ -61,7 +61,7 @@ export function getSocketAddressOnENR(enr: ENR): SocketAddress | undefined { } } -export function setSocketAddressOnENR(enr: ENR, s: SocketAddress): void { +export function setSocketAddressOnENR(enr: SignableENR, s: SocketAddress): void { switch (s.ip.type) { case 4: enr.set("ip", s.ip.octets); diff --git a/test/e2e/connect.test.ts b/test/e2e/connect.test.ts index 99c20b19..2ef19280 100644 --- a/test/e2e/connect.test.ts +++ b/test/e2e/connect.test.ts @@ -4,7 +4,7 @@ import { multiaddr } from "@multiformats/multiaddr"; import { PeerId } from "@libp2p/interface-peer-id"; import { createFromPrivKey } from "@libp2p/peer-id-factory"; import { unmarshalPrivateKey } from "@libp2p/crypto/keys"; -import { createKeypairFromPeerId, Discv5, ENR } from "../../src/index.js"; +import { Discv5, SignableENR } from "../../src/index.js"; let nodeIdx = 0; const portBase = 10000; @@ -14,14 +14,14 @@ describe("discv5 integration test", function () { type Node = { peerId: PeerId; - enr: ENR; + enr: SignableENR; discv5: Discv5; }; async function getDiscv5Node(): Promise { const idx = nodeIdx++; const port = portBase + idx; const peerId = await getPeerId(idx); - const enr = ENR.createFromPeerId(peerId); + const enr = SignableENR.createFromPeerId(peerId); const bindAddrUdp = `/ip4/127.0.0.1/udp/${port}`; const multiAddrUdp = multiaddr(bindAddrUdp); @@ -40,9 +40,6 @@ describe("discv5 integration test", function () { await discv5.start(); - // ensure the signature is cached - enr.encode(createKeypairFromPeerId(peerId).privateKey); - return { peerId, enr, discv5 }; } @@ -59,8 +56,8 @@ describe("discv5 integration test", function () { const node1 = await getDiscv5Node(); const node2 = await getDiscv5Node(); - node0.discv5.addEnr(node1.enr); - node1.discv5.addEnr(node2.enr); + node0.discv5.addEnr(node1.enr.toENR()); + node1.discv5.addEnr(node2.enr.toENR()); const nodes = await node0.discv5.findNode(node2.enr.nodeId); expect(nodes.map((n) => n.nodeId)).to.deep.equal([node2.enr.nodeId, node1.enr.nodeId], "Should find ENR of node2"); }); @@ -69,11 +66,11 @@ describe("discv5 integration test", function () { const node0 = await getDiscv5Node(); const node1 = await getDiscv5Node(); - node0.discv5.addEnr(node1.enr); + node0.discv5.addEnr(node1.enr.toENR()); // test a TALKRESP with no response try { - await node0.discv5.sendTalkReq(node1.enr, Buffer.from([0, 1, 2, 3]), "foo"); + await node0.discv5.sendTalkReq(node1.enr.toENR(), Buffer.from([0, 1, 2, 3]), "foo"); expect.fail("TALKREQ response should throw when no response is given"); } catch (e) { // expected @@ -84,7 +81,7 @@ describe("discv5 integration test", function () { node1.discv5.on("talkReqReceived", (nodeAddr, enr, request) => { node1.discv5.sendTalkResp(nodeAddr, request.id, expectedResp); }); - const resp = await node0.discv5.sendTalkReq(node1.enr, Buffer.from([0, 1, 2, 3]), "foo"); + const resp = await node0.discv5.sendTalkReq(node1.enr.toENR(), Buffer.from([0, 1, 2, 3]), "foo"); expect(resp).to.deep.equal(expectedResp); }); }); diff --git a/test/e2e/mainnetBootnodes.test.ts b/test/e2e/mainnetBootnodes.test.ts index 1536f9af..dbfc58c2 100644 --- a/test/e2e/mainnetBootnodes.test.ts +++ b/test/e2e/mainnetBootnodes.test.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { multiaddr } from "@multiformats/multiaddr"; import { createSecp256k1PeerId, createFromPrivKey } from "@libp2p/peer-id-factory"; import { unmarshalPrivateKey } from "@libp2p/crypto/keys"; -import { Discv5, ENR } from "../../src/index.js"; +import { Discv5, ENR, SignableENR } from "../../src/index.js"; let port = 9000; @@ -23,7 +23,7 @@ describe("discv5 integration test", function () { ) ); - const enr = ENR.createFromPeerId(peerId); + const enr = SignableENR.createFromPeerId(peerId); const bindAddrUdp = `/ip4/0.0.0.0/udp/${port++}`; const multiAddrUdp = multiaddr(bindAddrUdp); diff --git a/test/unit/enr.test.ts b/test/unit/enr.test.ts deleted file mode 100644 index f29a39fd..00000000 --- a/test/unit/enr.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* eslint-env mocha */ -import { expect } from "chai"; -import { multiaddr } from "@multiformats/multiaddr"; -import { createSecp256k1PeerId } from "@libp2p/peer-id-factory"; -import { ENR, v4 } from "../../src/enr/index.js"; - -describe("ENR", () => { - let seq: bigint; - let privateKey: Buffer; - let record: ENR; - - beforeEach(() => { - seq = 1n; - privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); - record = ENR.createV4(v4.publicKey(privateKey)); - record.set("ip", Buffer.from("7f000001", "hex")); - record.set("udp", Buffer.from((30303).toString(16), "hex")); - record.seq = seq; - }); - - it("should properly compute the node id", () => { - expect(record.nodeId).to.equal("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"); - }); - - it("should encode/decode to RLP encoding", () => { - const decoded = ENR.decode(record.encode(privateKey)); - expect(decoded).to.deep.equal(record); - }); - - it("should encode/decode to text encoding", () => { - // spec enr https://eips.ethereum.org/EIPS/eip-778 - const testTxt = - "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; - const decoded = ENR.decodeTxt(testTxt); - expect(decoded.udp).to.be.equal(30303); - expect(decoded.ip).to.be.equal("127.0.0.1"); - expect(decoded).to.deep.equal(record); - expect(record.encodeTxt(privateKey)).to.equal(testTxt); - }); -}); -describe("ENR Multiformats support", () => { - let privateKey: Buffer; - let record: ENR; - - beforeEach(() => { - privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); - record = ENR.createV4(v4.publicKey(privateKey)); - }); - - it("should get / set UDP multiaddr", () => { - const multi0 = multiaddr("/ip4/127.0.0.1/udp/30303"); - const tuples0 = multi0.tuples(); - - if (!tuples0[0][1] || !tuples0[1][1]) { - throw new Error("invalid multiaddr"); - } - // set underlying records - 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()); - // 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()); - // and get the underlying records - const tuples1 = multi1.tuples(); - expect(record.get("ip")).to.deep.equal(tuples1[0][1]); - expect(record.get("udp")).to.deep.equal(tuples1[1][1]); - }); - it("should get / set TCP multiaddr", () => { - const multi0 = multiaddr("/ip4/127.0.0.1/tcp/30303"); - const tuples0 = multi0.tuples(); - - if (!tuples0[0][1] || !tuples0[1][1]) { - throw new Error("invalid multiaddr"); - } - - // set underlying records - 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()); - // 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()); - // and get the underlying records - const tuples1 = multi1.tuples(); - expect(record.get("ip")).to.deep.equal(tuples1[0][1]); - expect(record.get("tcp")).to.deep.equal(tuples1[1][1]); - }); - - it("should properly handle enr key set", () => { - record.encode(privateKey); - record.set("tcp", Uint8Array.from([0, 1])); - expect(() => record.encode()).to.throw(); - }); - - it("should properly handle enr key delete", () => { - record.set("tcp", Uint8Array.from([0, 1])); - record.encode(privateKey); - record.delete("tcp"); - expect(() => record.encode()).to.throw(); - }); - - describe("location multiaddr", async () => { - const ip4 = "127.0.0.1"; - const ip6 = "::1"; - const tcp = 8080; - const udp = 8080; - - const peerId = await createSecp256k1PeerId(); - const enr = ENR.createFromPeerId(peerId); - enr.ip = ip4; - enr.ip6 = ip6; - enr.tcp = tcp; - enr.udp = udp; - enr.tcp6 = tcp; - enr.udp6 = udp; - - it("should properly create location multiaddrs - udp4", () => { - expect(enr.getLocationMultiaddr("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}`)); - }); - - it("should properly create location multiaddrs - udp6", () => { - expect(enr.getLocationMultiaddr("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}`)); - }); - - it("should properly create location multiaddrs - udp", () => { - // default to ip4 - expect(enr.getLocationMultiaddr("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(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(multiaddr(`/ip4/${ip4}/udp/${udp}`)); - enr.ip6 = ip6; - }); - - it("should properly create location multiaddrs - tcp", () => { - // default to ip4 - expect(enr.getLocationMultiaddr("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(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(multiaddr(`/ip4/${ip4}/tcp/${tcp}`)); - enr.ip6 = ip6; - }); - }); -}); diff --git a/test/unit/enr/enr.test.ts b/test/unit/enr/enr.test.ts index 0179d48a..7bd37e60 100644 --- a/test/unit/enr/enr.test.ts +++ b/test/unit/enr/enr.test.ts @@ -1,19 +1,191 @@ -import { expect, assert } from "chai"; -import { ENR } from "../../../src/enr/enr.js"; -import { createKeypairFromPeerId } from "../../../src/keypair/index.js"; -import { toHex } from "../../../src/util/index.js"; -import { ERR_INVALID_ID } from "../../../src/enr/constants.js"; +/* eslint-env mocha */ +import { expect } from "chai"; import { multiaddr } from "@multiformats/multiaddr"; import { createSecp256k1PeerId } from "@libp2p/peer-id-factory"; +import { BaseENR, ENR, SignableENR, v4 } from "../../../src/enr/index.js"; +import { createKeypair, KeypairType, toHex } from "../../../src/index.js"; + +describe("ENR spec test vector", () => { + // spec enr https://eips.ethereum.org/EIPS/eip-778 + const privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); + const publicKey = v4.publicKey(privateKey); + const keypair = createKeypair(KeypairType.Secp256k1, privateKey, publicKey); + const text = + "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; + const seq = BigInt(1); + const signature = Buffer.from( + "7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", + "hex" + ); + const kvs = new Map( + Object.entries({ + id: Buffer.from("v4"), + secp256k1: publicKey, + ip: Buffer.from("7f000001", "hex"), + udp: Buffer.from((30303).toString(16), "hex"), + }) + ); + const nodeId = "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"; + + it("should properly round trip decode and encode", () => { + expect(ENR.decodeTxt(text).encodeTxt()).to.equal(text); + expect(SignableENR.decodeTxt(text, keypair).encodeTxt()).to.equal(text); + }); + it("should properly round trip to/from object", () => { + const enr = new ENR(kvs, seq, signature); + expect(ENR.fromObject(enr.toObject())).to.deep.equal(enr); + + const signableEnr = new SignableENR(kvs, seq, keypair); + expect(SignableENR.fromObject(signableEnr.toObject())).to.deep.equal(signableEnr); + }); + + it("should properly create and encode", () => { + expect(new SignableENR(kvs, seq, keypair).encodeTxt()).to.equal(text); + }); + + it("should properly compute the node id", () => { + expect(ENR.decodeTxt(text).nodeId).to.equal(nodeId); + expect(SignableENR.decodeTxt(text, keypair).nodeId).to.equal(nodeId); + }); + + it("should properly decode values", () => { + function expectENRValuesMatch(enr: BaseENR): void { + expect(enr.udp).to.equal(30303); + expect(enr.ip).to.equal("127.0.0.1"); + expect(enr.seq).to.equal(seq); + expect(enr.signature).to.deep.equal(signature); + expect(enr.kvs.get("secp256k1")).to.deep.equal(publicKey); + expect(enr.publicKey).to.deep.equal(publicKey); + } + const enr = ENR.decodeTxt(text); + const signableEnr = SignableENR.decodeTxt(text, keypair); + expectENRValuesMatch(enr); + expectENRValuesMatch(signableEnr); + }); +}); + +describe("ENR multiaddr support", () => { + const privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); + const publicKey = v4.publicKey(privateKey); + const keypair = createKeypair(KeypairType.Secp256k1, privateKey, publicKey); + let record: SignableENR; + + beforeEach(() => { + record = SignableENR.createV4(keypair); + }); + + it("should get / set UDP multiaddr", () => { + const multi0 = multiaddr("/ip4/127.0.0.1/udp/30303"); + const tuples0 = multi0.tuples(); + + if (!tuples0[0][1] || !tuples0[1][1]) { + throw new Error("invalid multiaddr"); + } + // set underlying records + 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()); + // 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()); + // and get the underlying records + const tuples1 = multi1.tuples(); + expect(record.kvs.get("ip")).to.deep.equal(tuples1[0][1]); + expect(record.kvs.get("udp")).to.deep.equal(tuples1[1][1]); + }); + it("should get / set TCP multiaddr", () => { + const multi0 = multiaddr("/ip4/127.0.0.1/tcp/30303"); + const tuples0 = multi0.tuples(); + + if (!tuples0[0][1] || !tuples0[1][1]) { + throw new Error("invalid multiaddr"); + } + + // set underlying records + 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()); + // 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()); + // and get the underlying records + const tuples1 = multi1.tuples(); + expect(record.kvs.get("ip")).to.deep.equal(tuples1[0][1]); + expect(record.kvs.get("tcp")).to.deep.equal(tuples1[1][1]); + }); + + describe("location multiaddr", async () => { + const ip4 = "127.0.0.1"; + const ip6 = "::1"; + const tcp = 8080; + const udp = 8080; + + const peerId = await createSecp256k1PeerId(); + const enr = SignableENR.createFromPeerId(peerId); + enr.ip = ip4; + enr.ip6 = ip6; + enr.tcp = tcp; + enr.udp = udp; + enr.tcp6 = tcp; + enr.udp6 = udp; + + it("should properly create location multiaddrs - udp4", () => { + expect(enr.getLocationMultiaddr("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}`)); + }); + + it("should properly create location multiaddrs - udp6", () => { + expect(enr.getLocationMultiaddr("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}`)); + }); + + it("should properly create location multiaddrs - udp", () => { + // default to ip4 + expect(enr.getLocationMultiaddr("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(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(multiaddr(`/ip4/${ip4}/udp/${udp}`)); + enr.ip6 = ip6; + }); + + it("should properly create location multiaddrs - tcp", () => { + // default to ip4 + expect(enr.getLocationMultiaddr("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(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(multiaddr(`/ip4/${ip4}/tcp/${tcp}`)); + enr.ip6 = ip6; + }); + }); +}); describe("ENR", function () { describe("decodeTxt", () => { it("should encodeTxt and decodeTxt", async () => { const peerId = await createSecp256k1PeerId(); - const enr = ENR.createFromPeerId(peerId); - const keypair = createKeypairFromPeerId(peerId); + const enr = SignableENR.createFromPeerId(peerId); enr.setLocationMultiaddr(multiaddr("/ip4/18.223.219.100/udp/9000")); - const txt = enr.encodeTxt(keypair.privateKey); + const txt = enr.encodeTxt(); expect(txt.slice(0, 4)).to.be.equal("enr:"); const enr2 = ENR.decodeTxt(txt); expect(toHex(enr2.signature as Buffer)).to.be.equal(toHex(enr.signature as Buffer)); @@ -25,84 +197,43 @@ describe("ENR", function () { const txt = "enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg"; const enr = ENR.decodeTxt(txt); - const eth2 = enr.get("eth2") as Buffer; + const eth2 = enr.kvs.get("eth2") as Buffer; expect(eth2).to.not.be.undefined; expect(toHex(eth2)).to.be.equal("f6775d0700000113ffffffffffff1f00"); }); it("should encodeTxt and decodeTxt ipv6 enr successfully", async () => { const peerId = await createSecp256k1PeerId(); - const enr = ENR.createFromPeerId(peerId); + const enr = SignableENR.createFromPeerId(peerId); enr.setLocationMultiaddr(multiaddr("/ip6/aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa/udp/9000")); - const keypair = createKeypairFromPeerId(peerId); - const enr2 = ENR.decodeTxt(enr.encodeTxt(keypair.privateKey)); + const enr2 = ENR.decodeTxt(enr.encodeTxt()); expect(enr2.udp6).to.equal(9000); expect(enr2.ip6).to.equal("aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa:aaaa"); }); - it("should throw error - no id", () => { + it("should throw decoding error - no id", () => { try { const txt = Buffer.from( "656e723a2d435972595a62404b574342526c4179357a7a61445a584a42476b636e68344d486342465a6e75584e467264764a6a5830346a527a6a7a", "hex" ).toString(); ENR.decodeTxt(txt); - assert.fail("Expect error here"); + expect.fail("Expect error here"); } catch (err: any) { - expect(err.message).to.be.equal(ERR_INVALID_ID); + expect(err.message).to.be.equal("id not found"); } }); - it("should throw error - no public key", () => { + it("should throw decoding error - no public key", () => { try { const txt = "enr:-IS4QJ2d11eu6dC7E7LoXeLMgMP3kom1u3SE8esFSWvaHoo0dP1jg8O3-nx9ht-EO3CmG7L6OkHcMmoIh00IYWB92QABgmlkgnY0gmlwhH8AAAGJc2d11eu6dCsxoQIB_c-jQMOXsbjWkbN-kj99H57gfId5pfb4wa1qxwV4CIN1ZHCCIyk"; ENR.decodeTxt(txt); - assert.fail("Expect error here"); - } catch (err: any) { - expect(err.message).to.be.equal("Failed to verify enr: No public key"); - } - }); - }); - - describe("verify", () => { - it("should throw error - no id", () => { - try { - const enr = new ENR({}, BigInt(0), Buffer.alloc(0)); - enr.verify(Buffer.alloc(0), Buffer.alloc(0)); - assert.fail("Expect error here"); + expect.fail("Expect error here"); } catch (err: any) { - expect(err.message).to.be.equal(ERR_INVALID_ID); + expect(err.message).to.be.equal("Pubkey doesn't exist"); } }); - - it("should throw error - invalid id", () => { - try { - const enr = new ENR({ id: Buffer.from("v3") }, BigInt(0), Buffer.alloc(0)); - enr.verify(Buffer.alloc(0), Buffer.alloc(0)); - assert.fail("Expect error here"); - } catch (err: any) { - expect(err.message).to.be.equal(ERR_INVALID_ID); - } - }); - - it("should throw error - no public key", () => { - try { - const enr = new ENR({ id: Buffer.from("v4") }, BigInt(0), Buffer.alloc(0)); - enr.verify(Buffer.alloc(0), Buffer.alloc(0)); - assert.fail("Expect error here"); - } catch (err: any) { - expect(err.message).to.be.equal("Failed to verify enr: No public key"); - } - }); - - it("should return false", () => { - const txt = - "enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg"; - const enr = ENR.decodeTxt(txt); - // should have id and public key inside ENR - expect(enr.verify(Buffer.alloc(0), Buffer.alloc(0))).to.be.false; - }); }); }); diff --git a/test/unit/kademlia/kademlia.test.ts b/test/unit/kademlia/kademlia.test.ts index ebd0a624..7d2410fa 100644 --- a/test/unit/kademlia/kademlia.test.ts +++ b/test/unit/kademlia/kademlia.test.ts @@ -1,10 +1,11 @@ /* eslint-env mocha */ import { KademliaRoutingTable } from "../../../src/kademlia/kademlia.js"; import { expect } from "chai"; -import { ENR, v4, createNodeId } from "../../../src/enr/index.js"; +import { ENR, createNodeId, SignableENR } from "../../../src/enr/index.js"; import { EntryStatus, log2Distance } from "../../../src/kademlia/index.js"; import { randomBytes } from "@libp2p/crypto"; import { toBuffer } from "../../../src/util/index.js"; +import { generateKeypair, KeypairType } from "../../../src/index.js"; describe("Kademlia routing table", () => { const nodeId = createNodeId(Buffer.alloc(32)); @@ -118,7 +119,8 @@ describe("Kademlia routing table", () => { }); function randomENR(): ENR { - return ENR.createV4(v4.publicKey(v4.createPrivateKey())); + const keypair = generateKeypair(KeypairType.Secp256k1); + return SignableENR.createV4(keypair).toENR(); } function randomNodeId(): string { diff --git a/test/unit/message/codec.test.ts b/test/unit/message/codec.test.ts index 08bb0351..d4209984 100644 --- a/test/unit/message/codec.test.ts +++ b/test/unit/message/codec.test.ts @@ -91,7 +91,10 @@ describe("message", () => { it(`should encode/decode message type ${MessageType[message.type]}`, () => { const actual = encode(message); expect(actual).to.deep.equal(expected); - expect(decode(actual)).to.deep.equal(message); + const decoded = decode(actual); + // to allow for any cached inner objects to be populated + encode(decoded); + expect(decoded).to.deep.equal(message); }); } }); diff --git a/test/unit/service/service.test.ts b/test/unit/service/service.test.ts index 7666884d..dea9bc8b 100644 --- a/test/unit/service/service.test.ts +++ b/test/unit/service/service.test.ts @@ -3,13 +3,13 @@ import { expect } from "chai"; import { multiaddr } from "@multiformats/multiaddr"; import { Discv5 } from "../../../src/service/service.js"; -import { ENR } from "../../../src/enr/index.js"; +import { SignableENR } from "../../../src/enr/index.js"; import { generateKeypair, KeypairType, createPeerIdFromKeypair } from "../../../src/keypair/index.js"; describe("Discv5", async () => { const kp0 = generateKeypair(KeypairType.Secp256k1); const peerId0 = await createPeerIdFromKeypair(kp0); - const enr0 = ENR.createV4(kp0.publicKey); + const enr0 = SignableENR.createV4(kp0); const mu0 = multiaddr("/ip4/127.0.0.1/udp/40000"); const service0 = Discv5.create({ enr: enr0, peerId: peerId0, multiaddr: mu0 }); @@ -32,9 +32,9 @@ describe("Discv5", async () => { it("should add new enrs", async () => { const kp1 = generateKeypair(KeypairType.Secp256k1); - const enr1 = ENR.createV4(kp1.publicKey); - enr1.encode(kp1.privateKey); - service0.addEnr(enr1); + const enr1 = SignableENR.createV4(kp1); + enr1.encode(); + service0.addEnr(enr1.toENR()); expect(service0.kadValues().length).eq(1); }); @@ -42,7 +42,7 @@ describe("Discv5", async () => { this.timeout(10000); const kp1 = generateKeypair(KeypairType.Secp256k1); const peerId1 = await createPeerIdFromKeypair(kp1); - const enr1 = ENR.createV4(kp1.publicKey); + const enr1 = SignableENR.createV4(kp1); const mu1 = multiaddr("/ip4/127.0.0.1/udp/10360"); const addr1 = mu1.tuples(); @@ -52,16 +52,16 @@ describe("Discv5", async () => { enr1.set("ip", addr1[0][1]); enr1.set("udp", addr1[1][1]); - enr1.encode(kp1.privateKey); + enr1.encode(); const service1 = Discv5.create({ enr: enr1, peerId: peerId1, multiaddr: mu1 }); await service1.start(); for (let i = 0; i < 100; i++) { const kp = generateKeypair(KeypairType.Secp256k1); - const enr = ENR.createV4(kp.publicKey); - enr.encode(kp.privateKey); - service1.addEnr(enr); + const enr = SignableENR.createV4(kp); + enr.encode(); + service1.addEnr(enr.toENR()); } - service0.addEnr(enr1); + service0.addEnr(enr1.toENR()); await service0.findNode(Buffer.alloc(32).toString("hex")); await service1.stop(); }); diff --git a/test/unit/session/crypto.test.ts b/test/unit/session/crypto.test.ts index de7a6dc7..9be03b63 100644 --- a/test/unit/session/crypto.test.ts +++ b/test/unit/session/crypto.test.ts @@ -12,8 +12,8 @@ import { encryptMessage, decryptMessage, } from "../../../src/session/index.js"; -import { v4, ENR } from "../../../src/enr/index.js"; -import { KeypairType, createKeypair } from "../../../src/keypair/index.js"; +import { v4, SignableENR } from "../../../src/enr/index.js"; +import { KeypairType, createKeypair, generateKeypair } from "../../../src/keypair/index.js"; import { createNodeContact } from "../../../src/session/nodeInfo.js"; describe("session crypto", () => { @@ -50,14 +50,14 @@ describe("session crypto", () => { }); it("symmetric keys should be derived correctly", () => { - const sk1 = v4.createPrivateKey(); - const sk2 = v4.createPrivateKey(); - const enr1 = ENR.createV4(v4.publicKey(sk1)); - const enr2 = ENR.createV4(v4.publicKey(sk2)); + const kp1 = generateKeypair(KeypairType.Secp256k1); + const kp2 = generateKeypair(KeypairType.Secp256k1); + const enr1 = SignableENR.createV4(kp1); + const enr2 = SignableENR.createV4(kp2); const nonce = randomBytes(32); - const [a1, b1, pk] = generateSessionKeys(enr1.nodeId, createNodeContact(enr2), nonce); + const [a1, b1, pk] = generateSessionKeys(enr1.nodeId, createNodeContact(enr2.toENR()), nonce); const [a2, b2] = deriveKeysFromPubkey( - createKeypair(KeypairType.Secp256k1, sk2), + createKeypair(KeypairType.Secp256k1, kp2.privateKey), enr2.nodeId, enr1.nodeId, pk, diff --git a/test/unit/session/service.test.ts b/test/unit/session/service.test.ts index 22430c74..d1ac7ea3 100644 --- a/test/unit/session/service.test.ts +++ b/test/unit/session/service.test.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { Multiaddr, multiaddr } from "@multiformats/multiaddr"; import { createKeypair, KeypairType } from "../../../src/keypair/index.js"; -import { ENR } from "../../../src/enr/index.js"; +import { SignableENR } from "../../../src/enr/index.js"; import { createWhoAreYouPacket, IPacket, PacketType } from "../../../src/packet/index.js"; import { UDPTransportService } from "../../../src/transport/index.js"; import { SessionService } from "../../../src/session/index.js"; @@ -26,8 +26,8 @@ describe("session service", () => { const addr0 = multiaddr("/ip4/127.0.0.1/udp/49020"); const addr1 = multiaddr("/ip4/127.0.0.1/udp/49021"); - const enr0 = ENR.createV4(kp0.publicKey); - const enr1 = ENR.createV4(kp1.publicKey); + const enr0 = SignableENR.createV4(kp0); + const enr1 = SignableENR.createV4(kp1); enr0.setLocationMultiaddr(addr0); enr1.setLocationMultiaddr(addr1); @@ -71,11 +71,11 @@ describe("session service", () => { ); // send a who are you when requested service1.on("whoAreYouRequest", (nodeAddr, authTag) => { - service1.sendChallenge(nodeAddr, authTag, enr0); + service1.sendChallenge(nodeAddr, authTag, enr0.toENR()); }); const establishedSession = new Promise((resolve) => service1.once("established", (_, enr) => { - expect(enr).to.deep.equal(enr0); + expect(enr.encode()).to.deep.equal(enr0.encode()); resolve(); }) ); @@ -84,7 +84,7 @@ describe("session service", () => { resolve(); }) ); - service0.sendRequest(createNodeContact(enr1), createFindNodeMessage([0])); + service0.sendRequest(createNodeContact(enr1.toENR()), createFindNodeMessage([0])); await Promise.all([receivedRandom, receivedWhoAreYou, establishedSession, receivedMsg]); }); it("receiver should drop WhoAreYou packets from destinations without existing pending requests", async () => { diff --git a/test/unit/util/ip.test.ts b/test/unit/util/ip.test.ts index 4d189b13..a3d65845 100644 --- a/test/unit/util/ip.test.ts +++ b/test/unit/util/ip.test.ts @@ -1,6 +1,6 @@ import { multiaddr } from "@multiformats/multiaddr"; import { expect } from "chai"; -import { ENR } from "../../../src/index.js"; +import { generateKeypair, KeypairType, SignableENR } from "../../../src/index.js"; import { getSocketAddressOnENR, SocketAddress, @@ -133,7 +133,7 @@ describe("get/set SocketAddress on ENR", () => { }, port: 53, }; - const enr = new ENR(); + const enr = SignableENR.createV4(generateKeypair(KeypairType.Secp256k1)); expect(getSocketAddressOnENR(enr)).to.equal(undefined); setSocketAddressOnENR(enr, addr);