From fcfd85c86e9e4f45e17fb80e6ab1ace98cad2670 Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 4 Sep 2024 09:01:24 -0400 Subject: [PATCH] feat: add quic support --- packages/enr/src/enr.ts | 113 +++++++++++++++++++++++++---- packages/enr/test/unit/enr.test.ts | 47 ++++++++++++ 2 files changed, 144 insertions(+), 16 deletions(-) diff --git a/packages/enr/src/enr.ts b/packages/enr/src/enr.ts index 9ace062c..41bf8342 100644 --- a/packages/enr/src/enr.ts +++ b/packages/enr/src/enr.ts @@ -161,7 +161,8 @@ export function decodeTxt(encoded: string): ENRData { // IP / Protocol -export type Protocol = "udp" | "tcp" | "udp4" | "udp6" | "tcp4" | "tcp6"; +/** Protocols automagically supported by this library */ +export type Protocol = "udp" | "tcp" | "quic" | "udp4" | "udp6" | "tcp4" | "tcp6" | "quic4" | "quic6"; export function getIPValue(kvs: ReadonlyMap, key: string, multifmtStr: string): string | undefined { const raw = kvs.get(key); @@ -191,6 +192,57 @@ export function portToBuf(port: number): Uint8Array { return buf; } +export function parseLocationMultiaddr(ma: Multiaddr): { + family: 4 | 6; + ip: Uint8Array; + protoName: "udp" | "tcp" | "quic"; + protoVal: Uint8Array; +} { + const protoNames = ma.protoNames(); + const tuples = ma.tuples(); + let family: 4 | 6; + let protoName: "udp" | "tcp" | "quic"; + + if (protoNames[0] === "ip4") { + family = 4; + } else if (protoNames[0] === "ip6") { + family = 6; + } else { + throw new Error("Invalid multiaddr: must start with ip4 or ip6"); + } + if (tuples[0][1] == null) { + throw new Error("Invalid multiaddr: ip address is missing"); + } + const ip = tuples[0][1]; + + if (protoNames[1] === "udp") { + protoName = "udp"; + } else if (protoNames[1] === "tcp") { + protoName = "tcp"; + } else { + throw new Error("Invalid multiaddr: must have udp or tcp protocol"); + } + if (tuples[1][1] == null) { + throw new Error("Invalid multiaddr: udp or tcp port is missing"); + } + const protoVal = tuples[1][1]; + + if (protoNames.length === 3) { + if (protoNames[2] === "quic-v1") { + if (protoName !== "udp") { + throw new Error("Invalid multiaddr: quic protocol must be used with udp"); + } + protoName = "quic"; + } else { + throw new Error("Invalid multiaddr: unknown protocol"); + } + } else if (protoNames.length > 2) { + throw new Error("Invalid multiaddr: unknown protocol"); + } + + return { family, ip, protoName, protoVal }; +} + // Classes export abstract class BaseENR { @@ -228,6 +280,9 @@ export abstract class BaseENR { get udp(): number | undefined { return getProtocolValue(this.kvs, "udp"); } + get quic(): number | undefined { + return getProtocolValue(this.kvs, "quic"); + } get ip6(): string | undefined { return getIPValue(this.kvs, "ip6", "ip6"); } @@ -237,6 +292,9 @@ export abstract class BaseENR { get udp6(): number | undefined { return getProtocolValue(this.kvs, "udp6"); } + get quic6(): number | undefined { + return getProtocolValue(this.kvs, "quic6"); + } getLocationMultiaddr(protocol: Protocol): Multiaddr | undefined { if (protocol === "udp") { return this.getLocationMultiaddr("udp4") || this.getLocationMultiaddr("udp6"); @@ -244,6 +302,9 @@ export abstract class BaseENR { if (protocol === "tcp") { return this.getLocationMultiaddr("tcp4") || this.getLocationMultiaddr("tcp6"); } + if (protocol === "quic") { + return this.getLocationMultiaddr("quic4") || this.getLocationMultiaddr("quic6"); + } const isIpv6 = protocol.endsWith("6"); const ipVal = this.kvs.get(isIpv6 ? "ip6" : "ip"); if (!ipVal) { @@ -252,6 +313,7 @@ export abstract class BaseENR { const isUdp = protocol.startsWith("udp"); const isTcp = protocol.startsWith("tcp"); + const isQuic = protocol.startsWith("quic"); let protoName, protoVal; if (isUdp) { protoName = "udp"; @@ -259,6 +321,9 @@ export abstract class BaseENR { } else if (isTcp) { protoName = "tcp"; protoVal = isIpv6 ? this.kvs.get("tcp6") : this.kvs.get("tcp"); + } else if (isQuic) { + protoName = "udp"; + protoVal = isIpv6 ? this.kvs.get("quic6") : this.kvs.get("quic"); } else { return undefined; } @@ -282,7 +347,11 @@ export abstract class BaseENR { maBuf.set(protoBuf, 1 + ipByteLen); maBuf.set(protoVal, 1 + ipByteLen + protoBuf.length); - return multiaddr(maBuf); + const ma = multiaddr(maBuf); + if (isQuic) { + return ma.encapsulate("/quic-v1"); + } + return ma; } async getFullMultiaddr(protocol: Protocol): Promise { const locationMultiaddr = this.getLocationMultiaddr(protocol); @@ -504,6 +573,16 @@ export class SignableENR extends BaseENR { this.set("udp", portToBuf(port)); } } + get quic(): number | undefined { + return getProtocolValue(this.kvs, "quic"); + } + set quic(port: number | undefined) { + if (port === undefined) { + this.delete("quic"); + } else { + this.set("quic", portToBuf(port)); + } + } get ip6(): string | undefined { return getIPValue(this.kvs, "ip6", "ip6"); } @@ -534,23 +613,25 @@ export class SignableENR extends BaseENR { this.set("udp6", portToBuf(port)); } } - setLocationMultiaddr(multiaddr: Multiaddr): void { - const protoNames = multiaddr.protoNames(); - if (protoNames.length !== 2 && protoNames[1] !== "udp" && protoNames[1] !== "tcp") { - throw new Error("Invalid multiaddr"); - } - const tuples = multiaddr.tuples(); - if (!tuples[0][1] || !tuples[1][1]) { - throw new Error("Invalid multiaddr"); + get quic6(): number | undefined { + return getProtocolValue(this.kvs, "quic6"); + } + set quic6(port: number | undefined) { + if (port === undefined) { + this.delete("quic6"); + } else { + this.set("quic6", portToBuf(port)); } + } + setLocationMultiaddr(multiaddr: Multiaddr): void { + const { family, ip, protoName, protoVal } = parseLocationMultiaddr(multiaddr); - // IPv4 - if (tuples[0][0] === 4) { - this.set("ip", tuples[0][1]); - this.set(protoNames[1], tuples[1][1]); + if (family === 4) { + this.set("ip", ip); + this.set(protoName, protoVal); } else { - this.set("ip6", tuples[0][1]); - this.set(protoNames[1] + "6", tuples[1][1]); + this.set("ip6", ip); + this.set(protoName + "6", protoVal); } } diff --git a/packages/enr/test/unit/enr.test.ts b/packages/enr/test/unit/enr.test.ts index 7c2d5ec2..79af936d 100644 --- a/packages/enr/test/unit/enr.test.ts +++ b/packages/enr/test/unit/enr.test.ts @@ -117,12 +117,36 @@ describe("ENR multiaddr support", () => { expect(record.kvs.get("ip")).to.deep.equal(tuples1[0][1]); expect(record.kvs.get("tcp")).to.deep.equal(tuples1[1][1]); }); + it("should get / set QUIC multiaddr", () => { + const multi0 = multiaddr("/ip4/127.0.0.1/udp/30303/quic-v1"); + 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("quic", tuples0[1][1]); + // and get the multiaddr + expect(record.getLocationMultiaddr("quic")!.toString()).to.equal(multi0.toString()); + // set the multiaddr + const multi1 = multiaddr("/ip4/0.0.0.0/udp/30300/quic-v1"); + record.setLocationMultiaddr(multi1); + // and get the multiaddr + expect(record.getLocationMultiaddr("quic")!.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("quic")).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 quic = 8081; const peerId = await createSecp256k1PeerId(); const enr = SignableENR.createFromPeerId(peerId); @@ -130,8 +154,10 @@ describe("ENR multiaddr support", () => { enr.ip6 = ip6; enr.tcp = tcp; enr.udp = udp; + enr.quic = quic; enr.tcp6 = tcp; enr.udp6 = udp; + enr.quic6 = quic; it("should properly create location multiaddrs - udp4", () => { expect(enr.getLocationMultiaddr("udp4")).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${udp}`)); @@ -141,6 +167,10 @@ describe("ENR multiaddr support", () => { expect(enr.getLocationMultiaddr("tcp4")).to.deep.equal(multiaddr(`/ip4/${ip4}/tcp/${tcp}`)); }); + it("should properly create location multiaddrs - quic4", () => { + expect(enr.getLocationMultiaddr("quic4")).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${quic}/quic-v1`)); + }); + it("should properly create location multiaddrs - udp6", () => { expect(enr.getLocationMultiaddr("udp6")).to.deep.equal(multiaddr(`/ip6/${ip6}/udp/${udp}`)); }); @@ -149,6 +179,10 @@ describe("ENR multiaddr support", () => { expect(enr.getLocationMultiaddr("tcp6")).to.deep.equal(multiaddr(`/ip6/${ip6}/tcp/${tcp}`)); }); + it("should properly create location multiaddrs - quic6", () => { + expect(enr.getLocationMultiaddr("quic6")).to.deep.equal(multiaddr(`/ip6/${ip6}/udp/${quic}/quic-v1`)); + }); + it("should properly create location multiaddrs - udp", () => { // default to ip4 expect(enr.getLocationMultiaddr("udp")).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${udp}`)); @@ -174,6 +208,19 @@ describe("ENR multiaddr support", () => { expect(enr.getLocationMultiaddr("tcp")).to.deep.equal(multiaddr(`/ip4/${ip4}/tcp/${tcp}`)); enr.ip6 = ip6; }); + + it("should properly create location multiaddrs - quic", () => { + // default to ip4 + expect(enr.getLocationMultiaddr("quic")).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${quic}/quic-v1`)); + // if ip6 is set, use it + enr.ip = undefined; + expect(enr.getLocationMultiaddr("quic")).to.deep.equal(multiaddr(`/ip6/${ip6}/udp/${quic}/quic-v1`)); + // if ip6 does not exist, use ip4 + enr.ip6 = undefined; + enr.ip = ip4; + expect(enr.getLocationMultiaddr("quic")).to.deep.equal(multiaddr(`/ip4/${ip4}/udp/${quic}/quic-v1`)); + enr.ip6 = ip6; + }); }); });