diff --git a/packages/discv5/package.json b/packages/discv5/package.json index b4aa108f..10222a60 100644 --- a/packages/discv5/package.json +++ b/packages/discv5/package.json @@ -67,15 +67,14 @@ "devDependencies": {}, "dependencies": { "@chainsafe/enr": "^4.0.1", + "@ethereumjs/rlp": "^5.0.2", "@libp2p/crypto": "^5.0.1", "@libp2p/interface": "^2.0.1", "@multiformats/multiaddr": "^12.1.10", "@noble/hashes": "^1.7.0", "@noble/secp256k1": "^2.2.2", - "bigint-buffer": "^1.1.5", "debug": "^4.3.1", "lru-cache": "^10.1.0", - "rlp": "^2.2.6", "strict-event-emitter-types": "^2.0.0" } } diff --git a/packages/discv5/src/kademlia/util.ts b/packages/discv5/src/kademlia/util.ts index 68ec6cef..c5421215 100644 --- a/packages/discv5/src/kademlia/util.ts +++ b/packages/discv5/src/kademlia/util.ts @@ -1,14 +1,13 @@ -import { toBigIntBE } from "bigint-buffer"; -import { NodeId } from "@chainsafe/enr"; +import { bytesToBigint, NodeId } from "@chainsafe/enr"; -import { fromHex } from "../util/index.js"; import { NUM_BUCKETS } from "./constants.js"; +import { hexToBytes } from "ethereum-cryptography/utils.js"; /** * Computes the xor distance between two NodeIds */ export function distance(a: NodeId, b: NodeId): bigint { - return toBigIntBE(fromHex(a)) ^ toBigIntBE(fromHex(b)); + return bytesToBigint(hexToBytes(a)) ^ bytesToBigint(hexToBytes(b)); } export function log2Distance(a: NodeId, b: NodeId): number { diff --git a/packages/discv5/src/keypair/index.ts b/packages/discv5/src/keypair/index.ts index 36bbecd3..58d2a626 100644 --- a/packages/discv5/src/keypair/index.ts +++ b/packages/discv5/src/keypair/index.ts @@ -3,7 +3,6 @@ import { KeyType } from "@libp2p/interface"; import { IKeypair } from "./types.js"; import { ERR_TYPE_NOT_IMPLEMENTED } from "./constants.js"; import { Secp256k1Keypair } from "./secp256k1.js"; -import { toBuffer } from "../util/index.js"; export * from "./types.js"; export * from "./secp256k1.js"; @@ -33,8 +32,8 @@ export function createKeypair(init: KeypairInit): IKeypair { switch (init.type) { case "secp256k1": return new Secp256k1Keypair( - init.privateKey ? toBuffer(init.privateKey) : undefined, - init.publicKey ? toBuffer(init.publicKey) : undefined + init.privateKey ? init.privateKey : undefined, + init.publicKey ? init.publicKey : undefined ); default: throw new Error(ERR_TYPE_NOT_IMPLEMENTED); diff --git a/packages/discv5/src/keypair/secp256k1.ts b/packages/discv5/src/keypair/secp256k1.ts index f7b2c406..2626f9b8 100644 --- a/packages/discv5/src/keypair/secp256k1.ts +++ b/packages/discv5/src/keypair/secp256k1.ts @@ -2,24 +2,23 @@ import { KeyType } from "@libp2p/interface"; import { AbstractKeypair, IKeypair, IKeypairClass } from "./types.js"; import { ERR_INVALID_KEYPAIR_TYPE } from "./constants.js"; import { getDiscv5Crypto } from "../util/crypto.js"; -import { toBuffer } from "../util/index.js"; - -export function secp256k1PublicKeyToCompressed(publicKey: Buffer): Buffer { +import { concatBytes } from "@noble/hashes/utils"; +export function secp256k1PublicKeyToCompressed(publicKey: Uint8Array): Uint8Array { if (publicKey.length === 64) { - publicKey = Buffer.concat([Buffer.from([4]), publicKey]); + publicKey = concatBytes(Uint8Array.from([4]), publicKey); } - return toBuffer(getDiscv5Crypto().secp256k1.publicKeyConvert(publicKey, true)); + return getDiscv5Crypto().secp256k1.publicKeyConvert(publicKey, true); } -export function secp256k1PublicKeyToRaw(publicKey: Buffer): Buffer { - return toBuffer(getDiscv5Crypto().secp256k1.publicKeyConvert(publicKey, false)); +export function secp256k1PublicKeyToRaw(publicKey: Uint8Array): Uint8Array { + return getDiscv5Crypto().secp256k1.publicKeyConvert(publicKey, false); } export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair extends AbstractKeypair implements IKeypair { readonly type: KeyType; - constructor(privateKey?: Buffer, publicKey?: Buffer) { - let pub = publicKey ?? toBuffer(getDiscv5Crypto().secp256k1.publicKeyCreate(privateKey!)); + constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) { + let pub = publicKey ?? getDiscv5Crypto().secp256k1.publicKeyCreate(privateKey!); if (pub) { pub = secp256k1PublicKeyToCompressed(pub); } @@ -28,8 +27,8 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair extends Ab } static generate(): Secp256k1Keypair { - const privateKey = toBuffer(getDiscv5Crypto().secp256k1.generatePrivateKey()); - const publicKey = toBuffer(getDiscv5Crypto().secp256k1.publicKeyCreate(privateKey)); + const privateKey = getDiscv5Crypto().secp256k1.generatePrivateKey(); + const publicKey = getDiscv5Crypto().secp256k1.publicKeyCreate(privateKey); return new Secp256k1Keypair(privateKey, publicKey); } @@ -45,16 +44,16 @@ export const Secp256k1Keypair: IKeypairClass = class Secp256k1Keypair extends Ab } return true; } - sign(msg: Buffer): Buffer { - return toBuffer(getDiscv5Crypto().secp256k1.sign(msg, this.privateKey)); + sign(msg: Uint8Array): Uint8Array { + return getDiscv5Crypto().secp256k1.sign(msg, this.privateKey); } - verify(msg: Buffer, sig: Buffer): boolean { + verify(msg: Uint8Array, sig: Uint8Array): boolean { return getDiscv5Crypto().secp256k1.verify(this.publicKey, msg, sig); } - deriveSecret(keypair: IKeypair): Buffer { + deriveSecret(keypair: IKeypair): Uint8Array { if (keypair.type !== this.type) { throw new Error(ERR_INVALID_KEYPAIR_TYPE); } - return toBuffer(getDiscv5Crypto().secp256k1.deriveSecret(this.privateKey, keypair.publicKey)); + return getDiscv5Crypto().secp256k1.deriveSecret(this.privateKey, keypair.publicKey); } }; diff --git a/packages/discv5/src/keypair/types.ts b/packages/discv5/src/keypair/types.ts index 612796c9..bfd73f1b 100644 --- a/packages/discv5/src/keypair/types.ts +++ b/packages/discv5/src/keypair/types.ts @@ -2,25 +2,25 @@ import { KeyType } from "@libp2p/interface"; export interface IKeypair { type: KeyType; - privateKey: Buffer; - publicKey: Buffer; + privateKey: Uint8Array; + publicKey: Uint8Array; privateKeyVerify(): boolean; publicKeyVerify(): boolean; - sign(msg: Buffer): Buffer; - verify(msg: Buffer, sig: Buffer): boolean; - deriveSecret(keypair: IKeypair): Buffer; + sign(msg: Uint8Array): Uint8Array; + verify(msg: Uint8Array, sig: Uint8Array): boolean; + deriveSecret(keypair: IKeypair): Uint8Array; hasPrivateKey(): boolean; } export interface IKeypairClass { - new (privateKey?: Buffer, publicKey?: Buffer): IKeypair; + new (privateKey?: Uint8Array, publicKey?: Uint8Array): IKeypair; generate(): IKeypair; } export abstract class AbstractKeypair { - readonly _privateKey?: Buffer; - readonly _publicKey?: Buffer; - constructor(privateKey?: Buffer, publicKey?: Buffer) { + readonly _privateKey?: Uint8Array; + readonly _publicKey?: Uint8Array; + constructor(privateKey?: Uint8Array, publicKey?: Uint8Array) { if ((this._privateKey = privateKey) && !this.privateKeyVerify()) { throw new Error("Invalid private key"); } @@ -28,13 +28,13 @@ export abstract class AbstractKeypair { throw new Error("Invalid private key"); } } - get privateKey(): Buffer { + get privateKey(): Uint8Array { if (!this._privateKey) { throw new Error(); } return this._privateKey; } - get publicKey(): Buffer { + get publicKey(): Uint8Array { if (!this._publicKey) { throw new Error(); } diff --git a/packages/discv5/src/message/create.ts b/packages/discv5/src/message/create.ts index 6d45500b..5f8059a3 100644 --- a/packages/discv5/src/message/create.ts +++ b/packages/discv5/src/message/create.ts @@ -1,6 +1,5 @@ -import { randomBytes } from "@noble/hashes/utils"; -import { toBigIntBE } from "bigint-buffer"; -import { SequenceNumber, ENR } from "@chainsafe/enr"; +import { randomBytes, toBytes } from "@noble/hashes/utils"; +import { bytesToBigint, SequenceNumber, ENR } from "@chainsafe/enr"; import { RequestId, @@ -11,10 +10,9 @@ import { ITalkReqMessage, ITalkRespMessage, } from "./types.js"; -import { toBuffer } from "../index.js"; export function createRequestId(): RequestId { - return toBigIntBE(toBuffer(randomBytes(8))); + return bytesToBigint(randomBytes(8)); } export function createPingMessage(enrSeq: SequenceNumber): IPingMessage { @@ -46,14 +44,14 @@ export function createTalkRequestMessage(request: string | Uint8Array, protocol: return { type: MessageType.TALKREQ, id: createRequestId(), - protocol: Buffer.from(protocol), - request: Buffer.from(request), + protocol: toBytes(protocol), + request: toBytes(request), }; } export function createTalkResponseMessage(requestId: RequestId, payload: Uint8Array): ITalkRespMessage { return { type: MessageType.TALKRESP, id: requestId, - response: toBuffer(payload), + response: payload, }; } diff --git a/packages/discv5/src/message/decode.ts b/packages/discv5/src/message/decode.ts index bf47634e..fb45d3f3 100644 --- a/packages/discv5/src/message/decode.ts +++ b/packages/discv5/src/message/decode.ts @@ -1,6 +1,5 @@ -import * as RLP from "rlp"; -import { toBigIntBE } from "bigint-buffer"; -import { ENR } from "@chainsafe/enr"; +import * as RLP from "@ethereumjs/rlp"; +import { bytesToBigint, ENR } from "@chainsafe/enr"; import { IPingMessage, IPongMessage, @@ -19,7 +18,7 @@ import { ipFromBytes } from "../util/ip.js"; const ERR_INVALID_MESSAGE = "invalid message"; -export function decode(data: Buffer): Message { +export function decode(data: Uint8Array): Message { const type = data[0]; switch (type) { case MessageType.PING: @@ -47,20 +46,20 @@ export function decode(data: Buffer): Message { } } -function decodePing(data: Buffer): IPingMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodePing(data: Uint8Array): IPingMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 2) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.PING, - id: toBigIntBE(rlpRaw[0]), - enrSeq: toBigIntBE(rlpRaw[1]), + id: bytesToBigint(rlpRaw[0]), + enrSeq: bytesToBigint(rlpRaw[1]), }; } -function decodePong(data: Buffer): IPongMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodePong(data: Uint8Array): IPongMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 4) { throw new Error(ERR_INVALID_MESSAGE); } @@ -75,117 +74,117 @@ function decodePong(data: Buffer): IPongMessage { if (rlpRaw[3].length > 2) { throw new Error(ERR_INVALID_MESSAGE); } - const port = rlpRaw[3].length ? rlpRaw[3].readUIntBE(0, rlpRaw[3].length) : 0; + const port = rlpRaw[3].length ? Number(bytesToBigint(rlpRaw[3])) : 0; return { type: MessageType.PONG, - id: toBigIntBE(rlpRaw[0]), - enrSeq: toBigIntBE(rlpRaw[1]), + id: bytesToBigint(rlpRaw[0]), + enrSeq: bytesToBigint(rlpRaw[1]), addr: { ip, port }, }; } -function decodeFindNode(data: Buffer): IFindNodeMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodeFindNode(data: Uint8Array): IFindNodeMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 2) { throw new Error(ERR_INVALID_MESSAGE); } if (!Array.isArray(rlpRaw[1])) { throw new Error(ERR_INVALID_MESSAGE); } - const distances = (rlpRaw[1] as unknown as Buffer[]).map((x) => (x.length ? x.readUIntBE(0, x.length) : 0)); + const distances = (rlpRaw[1] as Uint8Array[]).map((x) => (x.length ? Number(bytesToBigint(x)) : 0)); return { type: MessageType.FINDNODE, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), distances, }; } -function decodeNodes(data: Buffer): INodesMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as RLP.Decoded; +function decodeNodes(data: Uint8Array): INodesMessage { + const rlpRaw = RLP.decode(data.slice(1)) as RLP.NestedUint8Array; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 3 || !Array.isArray(rlpRaw[2])) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.NODES, - id: toBigIntBE(rlpRaw[0]), - total: rlpRaw[1].length ? rlpRaw[1].readUIntBE(0, rlpRaw[1].length) : 0, - enrs: rlpRaw[2].map((enrRaw) => ENR.decodeFromValues(enrRaw)), + id: bytesToBigint(rlpRaw[0] as Uint8Array), + total: rlpRaw[1].length ? Number(bytesToBigint(rlpRaw[1] as Uint8Array)) : 0, + enrs: rlpRaw[2].map((enrRaw) => ENR.decodeFromValues(enrRaw as Uint8Array[])), }; } -function decodeTalkReq(data: Buffer): ITalkReqMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as RLP.Decoded; +function decodeTalkReq(data: Uint8Array): ITalkReqMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 3) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.TALKREQ, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), protocol: rlpRaw[1], request: rlpRaw[2], }; } -function decodeTalkResp(data: Buffer): ITalkRespMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as RLP.Decoded; +function decodeTalkResp(data: Uint8Array): ITalkRespMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 2) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.TALKRESP, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), response: rlpRaw[1], }; } -function decodeRegTopic(data: Buffer): IRegTopicMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodeRegTopic(data: Uint8Array): IRegTopicMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 4 || !Array.isArray(rlpRaw[2])) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.REGTOPIC, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), topic: rlpRaw[1], - enr: ENR.decodeFromValues(rlpRaw[2] as unknown as Buffer[]), + enr: ENR.decodeFromValues(rlpRaw[2] as Uint8Array[]), ticket: rlpRaw[3], }; } -function decodeTicket(data: Buffer): ITicketMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodeTicket(data: Uint8Array): ITicketMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 3) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.TICKET, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), ticket: rlpRaw[1], - waitTime: rlpRaw[2].length ? rlpRaw[2].readUIntBE(0, rlpRaw[2].length) : 0, + waitTime: rlpRaw[2].length ? Number(bytesToBigint(rlpRaw[2] as Uint8Array)) : 0, }; } -function decodeRegConfirmation(data: Buffer): IRegConfirmationMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodeRegConfirmation(data: Uint8Array): IRegConfirmationMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 2) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.REGCONFIRMATION, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), topic: rlpRaw[1], }; } -function decodeTopicQuery(data: Buffer): ITopicQueryMessage { - const rlpRaw = RLP.decode(data.slice(1)) as unknown as Buffer[]; +function decodeTopicQuery(data: Uint8Array): ITopicQueryMessage { + const rlpRaw = RLP.decode(data.slice(1)) as Uint8Array[]; if (!Array.isArray(rlpRaw) || rlpRaw.length !== 2) { throw new Error(ERR_INVALID_MESSAGE); } return { type: MessageType.TOPICQUERY, - id: toBigIntBE(rlpRaw[0]), + id: bytesToBigint(rlpRaw[0]), topic: rlpRaw[1], }; } diff --git a/packages/discv5/src/message/encode.ts b/packages/discv5/src/message/encode.ts index 858e1e33..4d21ff1b 100644 --- a/packages/discv5/src/message/encode.ts +++ b/packages/discv5/src/message/encode.ts @@ -1,6 +1,6 @@ -import * as RLP from "rlp"; +import * as RLP from "@ethereumjs/rlp"; import { ipToBytes } from "../util/ip.js"; - +import { concatBytes } from "@noble/hashes/utils"; import { IPingMessage, IPongMessage, @@ -15,8 +15,9 @@ import { ITalkReqMessage, ITalkRespMessage, } from "./types.js"; +import { bigintToBytes } from "@chainsafe/enr"; -export function encode(message: Message): Buffer { +export function encode(message: Message): Uint8Array { switch (message.type) { case MessageType.PING: return encodePingMessage(message as IPingMessage); @@ -41,69 +42,54 @@ export function encode(message: Message): Buffer { } } -// TODO remove when rlp supports bigint encoding directly -function toBuffer(n: bigint): Buffer { - let hex = n.toString(16); - if (hex.length % 2 === 1) { - hex = "0" + hex; - } - return Buffer.from(hex, "hex"); -} - -export function encodePingMessage(m: IPingMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.PING]), RLP.encode([toBuffer(m.id), toBuffer(m.enrSeq)])]); +export function encodePingMessage(m: IPingMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.PING]), RLP.encode([bigintToBytes(m.id), bigintToBytes(m.enrSeq)])); } -export function encodePongMessage(m: IPongMessage): Buffer { +export function encodePongMessage(m: IPongMessage): Uint8Array { if (m.addr.port < 0 || m.addr.port > 65535) { throw new Error("invalid port for encoding"); } - return Buffer.concat([ - Buffer.from([MessageType.PONG]), - RLP.encode([ - // - toBuffer(m.id), - toBuffer(m.enrSeq), - ipToBytes(m.addr.ip), - m.addr.port, - ]), - ]); + return concatBytes( + Uint8Array.from([MessageType.PONG]), + RLP.encode([bigintToBytes(m.id), bigintToBytes(m.enrSeq), ipToBytes(m.addr.ip), m.addr.port]) + ); } -export function encodeFindNodeMessage(m: IFindNodeMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.FINDNODE]), RLP.encode([toBuffer(m.id), m.distances])]); +export function encodeFindNodeMessage(m: IFindNodeMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.FINDNODE]), RLP.encode([bigintToBytes(m.id), m.distances])); } -export function encodeNodesMessage(m: INodesMessage): Buffer { - return Buffer.concat([ - Buffer.from([MessageType.NODES]), - RLP.encode([toBuffer(m.id), m.total, m.enrs.map((enr) => enr.encodeToValues())]), - ]); +export function encodeNodesMessage(m: INodesMessage): Uint8Array { + return concatBytes( + Uint8Array.from([MessageType.NODES]), + RLP.encode([bigintToBytes(m.id), m.total, m.enrs.map((enr) => enr.encodeToValues())]) + ); } -export function encodeTalkReqMessage(m: ITalkReqMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.TALKREQ]), RLP.encode([toBuffer(m.id), m.protocol, m.request])]); +export function encodeTalkReqMessage(m: ITalkReqMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.TALKREQ]), RLP.encode([bigintToBytes(m.id), m.protocol, m.request])); } -export function encodeTalkRespMessage(m: ITalkRespMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.TALKRESP]), RLP.encode([toBuffer(m.id), m.response])]); +export function encodeTalkRespMessage(m: ITalkRespMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.TALKRESP]), RLP.encode([bigintToBytes(m.id), m.response])); } -export function encodeRegTopicMessage(m: IRegTopicMessage): Buffer { - return Buffer.concat([ - Buffer.from([MessageType.REGTOPIC]), - RLP.encode([toBuffer(m.id), m.topic, m.enr.encodeToValues(), m.ticket]), - ]); +export function encodeRegTopicMessage(m: IRegTopicMessage): Uint8Array { + return concatBytes( + Uint8Array.from([MessageType.REGTOPIC]), + RLP.encode([bigintToBytes(m.id), m.topic, m.enr.encodeToValues(), m.ticket]) + ); } -export function encodeTicketMessage(m: ITicketMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.TICKET]), RLP.encode([toBuffer(m.id), m.ticket, m.waitTime])]); +export function encodeTicketMessage(m: ITicketMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.TICKET]), RLP.encode([bigintToBytes(m.id), m.ticket, m.waitTime])); } -export function encodeRegConfirmMessage(m: IRegConfirmationMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.REGCONFIRMATION]), RLP.encode([toBuffer(m.id), m.topic])]); +export function encodeRegConfirmMessage(m: IRegConfirmationMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.REGCONFIRMATION]), RLP.encode([bigintToBytes(m.id), m.topic])); } -export function encodeTopicQueryMessage(m: ITopicQueryMessage): Buffer { - return Buffer.concat([Buffer.from([MessageType.TOPICQUERY]), RLP.encode([toBuffer(m.id), m.topic])]); +export function encodeTopicQueryMessage(m: ITopicQueryMessage): Uint8Array { + return concatBytes(Uint8Array.from([MessageType.TOPICQUERY]), RLP.encode([bigintToBytes(m.id), m.topic])); } diff --git a/packages/discv5/src/message/types.ts b/packages/discv5/src/message/types.ts index d0cc0b61..0d4960fc 100644 --- a/packages/discv5/src/message/types.ts +++ b/packages/discv5/src/message/types.ts @@ -72,39 +72,39 @@ export interface INodesMessage { export interface ITalkReqMessage { type: MessageType.TALKREQ; id: RequestId; - protocol: Buffer; - request: Buffer; + protocol: Uint8Array; + request: Uint8Array; } export interface ITalkRespMessage { type: MessageType.TALKRESP; id: RequestId; - response: Buffer; + response: Uint8Array; } export interface IRegTopicMessage { type: MessageType.REGTOPIC; id: RequestId; - topic: Buffer; + topic: Uint8Array; enr: ENR; - ticket: Buffer; + ticket: Uint8Array; } export interface ITicketMessage { type: MessageType.TICKET; id: RequestId; - ticket: Buffer; + ticket: Uint8Array; waitTime: number; } export interface IRegConfirmationMessage { type: MessageType.REGCONFIRMATION; id: RequestId; - topic: Buffer; + topic: Uint8Array; } export interface ITopicQueryMessage { type: MessageType.TOPICQUERY; id: RequestId; - topic: Buffer; + topic: Uint8Array; } diff --git a/packages/discv5/src/packet/create.ts b/packages/discv5/src/packet/create.ts index 0f3a2c39..dc563d79 100644 --- a/packages/discv5/src/packet/create.ts +++ b/packages/discv5/src/packet/create.ts @@ -3,14 +3,13 @@ import { NodeId, SequenceNumber } from "@chainsafe/enr"; import { ID_NONCE_SIZE, MASKING_IV_SIZE, NONCE_SIZE } from "./constants.js"; import { encodeMessageAuthdata, encodeWhoAreYouAuthdata } from "./encode.js"; import { IHeader, IPacket, PacketType } from "./types.js"; -import { toBuffer } from "../index.js"; -export function createHeader(flag: PacketType, authdata: Buffer, nonce = randomBytes(NONCE_SIZE)): IHeader { +export function createHeader(flag: PacketType, authdata: Uint8Array, nonce = randomBytes(NONCE_SIZE)): IHeader { return { protocolId: "discv5", version: 1, flag, - nonce: toBuffer(nonce), + nonce: nonce, authdataSize: authdata.length, authdata, }; @@ -19,8 +18,8 @@ export function createHeader(flag: PacketType, authdata: Buffer, nonce = randomB export function createRandomPacket(srcId: NodeId): IPacket { const authdata = encodeMessageAuthdata({ srcId }); const header = createHeader(PacketType.Message, authdata); - const maskingIv = toBuffer(randomBytes(MASKING_IV_SIZE)); - const message = toBuffer(randomBytes(44)); + const maskingIv = randomBytes(MASKING_IV_SIZE); + const message = randomBytes(44); return { maskingIv, header, @@ -28,11 +27,11 @@ export function createRandomPacket(srcId: NodeId): IPacket { }; } -export function createWhoAreYouPacket(nonce: Buffer, enrSeq: SequenceNumber): IPacket { - const idNonce = toBuffer(randomBytes(ID_NONCE_SIZE)); +export function createWhoAreYouPacket(nonce: Uint8Array, enrSeq: SequenceNumber): IPacket { + const idNonce = randomBytes(ID_NONCE_SIZE); const authdata = encodeWhoAreYouAuthdata({ idNonce, enrSeq }); const header = createHeader(PacketType.WhoAreYou, authdata, nonce); - const maskingIv = toBuffer(randomBytes(MASKING_IV_SIZE)); + const maskingIv = randomBytes(MASKING_IV_SIZE); const message = Buffer.alloc(0); return { maskingIv, diff --git a/packages/discv5/src/packet/encode.ts b/packages/discv5/src/packet/encode.ts index 5fa29c22..74a5202a 100644 --- a/packages/discv5/src/packet/encode.ts +++ b/packages/discv5/src/packet/encode.ts @@ -1,7 +1,6 @@ import Crypto from "node:crypto"; -import { toBigIntBE, toBufferBE } from "bigint-buffer"; -import { bufferToNumber, CodeError, fromHex, numberToBuffer, toHex } from "../util/index.js"; +import { bytesToNumber, CodeError, numberToBytes } from "../util/index.js"; import { AUTHDATA_SIZE_SIZE, EPH_KEY_SIZE_SIZE, @@ -27,28 +26,30 @@ import { MIN_HANDSHAKE_AUTHDATA_SIZE, } from "./constants.js"; import { IHandshakeAuthdata, IHeader, IMessageAuthdata, IPacket, IWhoAreYouAuthdata, PacketType } from "./types.js"; +import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes, bytesToUtf8 } from "ethereum-cryptography/utils.js"; +import { bigintToBytes, bytesToBigint } from "@chainsafe/enr"; -export function encodePacket(destId: string, packet: IPacket): Buffer { - return Buffer.concat([packet.maskingIv, encodeHeader(destId, packet.maskingIv, packet.header), packet.message]); +export function encodePacket(destId: string, packet: IPacket): Uint8Array { + return concatBytes(packet.maskingIv, encodeHeader(destId, packet.maskingIv, packet.header), packet.message); } -export function encodeHeader(destId: string, maskingIv: Buffer, header: IHeader): Buffer { - const ctx = Crypto.createCipheriv("aes-128-ctr", fromHex(destId).slice(0, MASKING_KEY_SIZE), maskingIv); +export function encodeHeader(destId: string, maskingIv: Uint8Array, header: IHeader): Uint8Array { + const ctx = Crypto.createCipheriv("aes-128-ctr", hexToBytes(destId).slice(0, MASKING_KEY_SIZE), maskingIv); return ctx.update( - Buffer.concat([ + concatBytes( // static header - Buffer.from(header.protocolId, "ascii"), - numberToBuffer(header.version, VERSION_SIZE), - numberToBuffer(header.flag, FLAG_SIZE), + utf8ToBytes(header.protocolId), + numberToBytes(header.version, VERSION_SIZE), + numberToBytes(header.flag, FLAG_SIZE), header.nonce, - numberToBuffer(header.authdataSize, AUTHDATA_SIZE_SIZE), + numberToBytes(header.authdataSize, AUTHDATA_SIZE_SIZE), // authdata - header.authdata, - ]) + header.authdata + ) ); } -export function decodePacket(srcId: string, data: Buffer): IPacket { +export function decodePacket(srcId: string, data: Uint8Array): IPacket { if (data.length < MIN_PACKET_SIZE) { throw new CodeError(`Packet too small: ${data.length}`, ERR_TOO_SMALL); } @@ -64,30 +65,30 @@ export function decodePacket(srcId: string, data: Buffer): IPacket { maskingIv, header, message, - messageAd: Buffer.concat([maskingIv, headerBuf]), + messageAd: concatBytes(maskingIv, headerBuf), }; } /** * Return the decoded header and the header as a buffer */ -export function decodeHeader(srcId: string, maskingIv: Buffer, data: Buffer): [IHeader, Buffer] { - const ctx = Crypto.createDecipheriv("aes-128-ctr", fromHex(srcId).slice(0, MASKING_KEY_SIZE), maskingIv); +export function decodeHeader(srcId: string, maskingIv: Uint8Array, data: Uint8Array): [IHeader, Uint8Array] { + const ctx = Crypto.createDecipheriv("aes-128-ctr", hexToBytes(srcId).slice(0, MASKING_KEY_SIZE), maskingIv); // unmask the static header const staticHeaderBuf = ctx.update(data.slice(0, STATIC_HEADER_SIZE)); // validate the static header field by field - const protocolId = staticHeaderBuf.slice(0, PROTOCOL_SIZE).toString("ascii"); + const protocolId = bytesToUtf8(staticHeaderBuf.slice(0, PROTOCOL_SIZE)); if (protocolId !== "discv5") { throw new CodeError(`Invalid protocol id: ${protocolId}`, ERR_INVALID_PROTOCOL_ID); } - const version = bufferToNumber(staticHeaderBuf.slice(PROTOCOL_SIZE, PROTOCOL_SIZE + VERSION_SIZE), VERSION_SIZE); + const version = bytesToNumber(staticHeaderBuf.slice(PROTOCOL_SIZE, PROTOCOL_SIZE + VERSION_SIZE), VERSION_SIZE); if (version !== 1) { throw new CodeError(`Invalid version: ${version}`, ERR_INVALID_VERSION); } - const flag = bufferToNumber( + const flag = bytesToNumber( staticHeaderBuf.slice(PROTOCOL_SIZE + VERSION_SIZE, PROTOCOL_SIZE + VERSION_SIZE + FLAG_SIZE), FLAG_SIZE ); @@ -100,7 +101,7 @@ export function decodeHeader(srcId: string, maskingIv: Buffer, data: Buffer): [I PROTOCOL_SIZE + VERSION_SIZE + FLAG_SIZE + NONCE_SIZE ); - const authdataSize = bufferToNumber( + const authdataSize = bytesToNumber( staticHeaderBuf.slice(PROTOCOL_SIZE + VERSION_SIZE + FLAG_SIZE + NONCE_SIZE), AUTHDATA_SIZE_SIZE ); @@ -123,49 +124,52 @@ export function decodeHeader(srcId: string, maskingIv: Buffer, data: Buffer): [I // authdata -export function encodeWhoAreYouAuthdata(authdata: IWhoAreYouAuthdata): Buffer { - return Buffer.concat([authdata.idNonce, toBufferBE(authdata.enrSeq, 8)]); +export function encodeWhoAreYouAuthdata(authdata: IWhoAreYouAuthdata): Uint8Array { + // Pad sequence to 8 bytes + const seqBytes = new Uint8Array(8); + seqBytes.set(bigintToBytes(authdata.enrSeq), 0); + return concatBytes(authdata.idNonce, seqBytes); } -export function encodeMessageAuthdata(authdata: IMessageAuthdata): Buffer { - return fromHex(authdata.srcId); +export function encodeMessageAuthdata(authdata: IMessageAuthdata): Uint8Array { + return hexToBytes(authdata.srcId); } -export function encodeHandshakeAuthdata(authdata: IHandshakeAuthdata): Buffer { - return Buffer.concat([ - fromHex(authdata.srcId), - numberToBuffer(authdata.sigSize, SIG_SIZE_SIZE), - numberToBuffer(authdata.ephKeySize, EPH_KEY_SIZE_SIZE), +export function encodeHandshakeAuthdata(authdata: IHandshakeAuthdata): Uint8Array { + return concatBytes( + hexToBytes(authdata.srcId), + numberToBytes(authdata.sigSize, SIG_SIZE_SIZE), + numberToBytes(authdata.ephKeySize, EPH_KEY_SIZE_SIZE), authdata.idSignature, authdata.ephPubkey, - authdata.record || Buffer.alloc(0), - ]); + authdata.record || new Uint8Array(0) + ); } -export function decodeWhoAreYouAuthdata(data: Buffer): IWhoAreYouAuthdata { +export function decodeWhoAreYouAuthdata(data: Uint8Array): IWhoAreYouAuthdata { if (data.length !== WHOAREYOU_AUTHDATA_SIZE) { throw new CodeError(`Invalid authdata length: ${data.length}`, ERR_INVALID_AUTHDATA_SIZE); } return { idNonce: data.slice(0, ID_NONCE_SIZE), - enrSeq: toBigIntBE(data.slice(ID_NONCE_SIZE)), + enrSeq: bytesToBigint(data.slice(ID_NONCE_SIZE)), }; } -export function decodeMessageAuthdata(data: Buffer): IMessageAuthdata { +export function decodeMessageAuthdata(data: Uint8Array): IMessageAuthdata { if (data.length !== MESSAGE_AUTHDATA_SIZE) { throw new CodeError(`Invalid authdata length: ${data.length}`, ERR_INVALID_AUTHDATA_SIZE); } return { - srcId: toHex(data), + srcId: bytesToHex(data), }; } -export function decodeHandshakeAuthdata(data: Buffer): IHandshakeAuthdata { +export function decodeHandshakeAuthdata(data: Uint8Array): IHandshakeAuthdata { if (data.length < MIN_HANDSHAKE_AUTHDATA_SIZE) { throw new CodeError(`Invalid authdata length: ${data.length}`, ERR_INVALID_AUTHDATA_SIZE); } - const srcId = toHex(data.slice(0, 32)); + const srcId = bytesToHex(data.slice(0, 32)); const sigSize = data[32]; const ephKeySize = data[33]; const idSignature = data.slice(34, 34 + sigSize); @@ -185,14 +189,14 @@ export function decodeHandshakeAuthdata(data: Buffer): IHandshakeAuthdata { * Encode Challenge Data given masking IV and header * Challenge data doubles as message authenticated data */ -export function encodeChallengeData(maskingIv: Buffer, header: IHeader): Buffer { - return Buffer.concat([ +export function encodeChallengeData(maskingIv: Uint8Array, header: IHeader): Uint8Array { + return concatBytes( maskingIv, - Buffer.from(header.protocolId), - numberToBuffer(header.version, VERSION_SIZE), - numberToBuffer(header.flag, FLAG_SIZE), + utf8ToBytes(header.protocolId), + numberToBytes(header.version, VERSION_SIZE), + numberToBytes(header.flag, FLAG_SIZE), header.nonce, - numberToBuffer(header.authdataSize, AUTHDATA_SIZE_SIZE), - header.authdata, - ]); + numberToBytes(header.authdataSize, AUTHDATA_SIZE_SIZE), + header.authdata + ); } diff --git a/packages/discv5/src/packet/types.ts b/packages/discv5/src/packet/types.ts index 6d21f5a5..dbc95385 100644 --- a/packages/discv5/src/packet/types.ts +++ b/packages/discv5/src/packet/types.ts @@ -35,7 +35,7 @@ export interface IStaticHeader { /** * 12 bytes */ - nonce: Buffer; + nonce: Uint8Array; /** * 2 bytes */ @@ -43,7 +43,7 @@ export interface IStaticHeader { } export interface IHeader extends IStaticHeader { - authdata: Buffer; + authdata: Uint8Array; } // A IHeader contains an "authdata @@ -60,7 +60,7 @@ export interface IWhoAreYouAuthdata { /** * 16 bytes */ - idNonce: Buffer; + idNonce: Uint8Array; /** * 8 bytes */ @@ -71,15 +71,15 @@ export interface IHandshakeAuthdata { srcId: NodeId; sigSize: number; ephKeySize: number; - idSignature: Buffer; - ephPubkey: Buffer; + idSignature: Uint8Array; + ephPubkey: Uint8Array; // pre-encoded ENR record?: Uint8Array; } export interface IPacket { - maskingIv: Buffer; + maskingIv: Uint8Array; header: IHeader; - message: Buffer; - messageAd?: Buffer; + message: Uint8Array; + messageAd?: Uint8Array; } diff --git a/packages/discv5/src/service/service.ts b/packages/discv5/src/service/service.ts index 484eaf10..3fccafcb 100644 --- a/packages/discv5/src/service/service.ts +++ b/packages/discv5/src/service/service.ts @@ -36,7 +36,7 @@ import { RequestId, } from "../message/index.js"; import { AddrVotes } from "./addrVotes.js"; -import { CodeError, toBuffer } from "../util/index.js"; +import { CodeError } from "../util/index.js"; import { IDiscv5Config, defaultConfig } from "../config/index.js"; import { createNodeContact, getNodeAddress, getNodeId, INodeAddress, NodeContact } from "../session/nodeInfo.js"; import { @@ -324,7 +324,7 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { } public async findRandomNode(): Promise { - return await this.findNode(createNodeId(toBuffer(randomBytes(32)))); + return await this.findNode(createNodeId(randomBytes(32))); } /** @@ -398,7 +398,11 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { /** * Send TALKREQ message to dstId and returns response */ - public async sendTalkReq(remote: ENR | Multiaddr, payload: Buffer, protocol: string | Uint8Array): Promise { + public async sendTalkReq( + remote: ENR | Multiaddr, + payload: Uint8Array, + protocol: string | Uint8Array + ): Promise { const contact = createNodeContact(remote, this.ipMode); const request = createTalkRequestMessage(payload, protocol); @@ -407,7 +411,7 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { contact, request, callbackPromise: { - resolve: resolve as (val: Buffer) => void, + resolve: resolve as (val: Uint8Array) => void, reject, }, }); @@ -703,7 +707,7 @@ export class Discv5 extends (EventEmitter as { new (): Discv5EventEmitter }) { this.connectionUpdated(nodeId, { type: ConnectionStatusType.Connected, enr, direction }); }; - private handleWhoAreYouRequest = (nodeAddr: INodeAddress, nonce: Buffer): void => { + private handleWhoAreYouRequest = (nodeAddr: INodeAddress, nonce: Uint8Array): void => { // Check what our latest known ENR is for this node const enr = this.findEnr(nodeAddr.nodeId) ?? null; if (enr) { diff --git a/packages/discv5/src/service/types.ts b/packages/discv5/src/service/types.ts index 72f55e7e..8e379a87 100644 --- a/packages/discv5/src/service/types.ts +++ b/packages/discv5/src/service/types.ts @@ -91,7 +91,7 @@ export type PongResponse = { addr: SocketAddress; }; -export type ResponseType = Buffer | ENR[] | PongResponse; +export type ResponseType = Uint8Array | ENR[] | PongResponse; export function toResponseType(response: IPongMessage | INodesMessage | ITalkRespMessage): ResponseType { switch (response.type) { diff --git a/packages/discv5/src/session/crypto.ts b/packages/discv5/src/session/crypto.ts index 19058da0..1d17a84d 100644 --- a/packages/discv5/src/session/crypto.ts +++ b/packages/discv5/src/session/crypto.ts @@ -2,8 +2,8 @@ import Crypto from "node:crypto"; import { NodeId } from "@chainsafe/enr"; import { generateKeypair, IKeypair, createKeypair } from "../keypair/index.js"; -import { fromHex, toBuffer } from "../util/index.js"; import { getDiscv5Crypto } from "../util/crypto.js"; +import { concatBytes, hexToBytes, utf8ToBytes } from "ethereum-cryptography/utils.js"; // Implementation for generating session keys in the Discv5 protocol. // Currently, Diffie-Hellman key agreement is performed with known public key types. Session keys @@ -25,8 +25,8 @@ export function generateSessionKeys( localId: NodeId, remoteId: NodeId, remotePubkey: IKeypair, - challengeData: Buffer -): [Buffer, Buffer, Buffer] { + challengeData: Uint8Array +): [Uint8Array, Uint8Array, Uint8Array] { const ephemKeypair = generateKeypair(remotePubkey.type); const secret = ephemKeypair.deriveSecret(remotePubkey); /* TODO possibly not needed, check tests @@ -35,18 +35,25 @@ export function generateSessionKeys( ? secp256k1PublicKeyToCompressed(ephemKeypair.publicKey) : ephemKeypair.publicKey; */ - return [...deriveKey(secret, localId, remoteId, challengeData), ephemKeypair.publicKey] as [Buffer, Buffer, Buffer]; + return [...deriveKey(secret, localId, remoteId, challengeData), ephemKeypair.publicKey] as [ + Uint8Array, + Uint8Array, + Uint8Array + ]; } -export function deriveKey(secret: Buffer, firstId: NodeId, secondId: NodeId, challengeData: Buffer): [Buffer, Buffer] { - const info = Buffer.concat([Buffer.from(KEY_AGREEMENT_STRING), fromHex(firstId), fromHex(secondId)]); - const output = toBuffer( - getDiscv5Crypto().hkdf.expand( - getDiscv5Crypto().sha256, - getDiscv5Crypto().hkdf.extract(getDiscv5Crypto().sha256, secret, challengeData), - info, - 2 * KEY_LENGTH - ) +export function deriveKey( + secret: Uint8Array, + firstId: NodeId, + secondId: NodeId, + challengeData: Uint8Array +): [Uint8Array, Uint8Array] { + const info = concatBytes(utf8ToBytes(KEY_AGREEMENT_STRING), hexToBytes(firstId), hexToBytes(secondId)); + const output = getDiscv5Crypto().hkdf.expand( + getDiscv5Crypto().sha256, + getDiscv5Crypto().hkdf.extract(getDiscv5Crypto().sha256, secret, challengeData), + info, + 2 * KEY_LENGTH ); return [output.slice(0, KEY_LENGTH), output.slice(KEY_LENGTH, 2 * KEY_LENGTH)]; } @@ -55,15 +62,20 @@ export function deriveKeysFromPubkey( kpriv: IKeypair, localId: NodeId, remoteId: NodeId, - ephemPK: Buffer, - challengeData: Buffer -): [Buffer, Buffer] { + ephemPK: Uint8Array, + challengeData: Uint8Array +): [Uint8Array, Uint8Array] { const secret = kpriv.deriveSecret(createKeypair({ type: kpriv.type, publicKey: ephemPK })); return deriveKey(secret, remoteId, localId, challengeData); } // Generates a signature given a keypair. -export function idSign(kpriv: IKeypair, challengeData: Buffer, ephemPK: Buffer, destNodeId: NodeId): Buffer { +export function idSign( + kpriv: IKeypair, + challengeData: Uint8Array, + ephemPK: Uint8Array, + destNodeId: NodeId +): Uint8Array { const signingNonce = generateIdSignatureInput(challengeData, ephemPK, destNodeId); return kpriv.sign(signingNonce); } @@ -71,40 +83,40 @@ export function idSign(kpriv: IKeypair, challengeData: Buffer, ephemPK: Buffer, // Verifies the id signature export function idVerify( kpub: IKeypair, - challengeData: Buffer, - remoteEphemPK: Buffer, + challengeData: Uint8Array, + remoteEphemPK: Uint8Array, srcNodeId: NodeId, - sig: Buffer + sig: Uint8Array ): boolean { const signingNonce = generateIdSignatureInput(challengeData, remoteEphemPK, srcNodeId); return kpub.verify(signingNonce, sig); } -export function generateIdSignatureInput(challengeData: Buffer, ephemPK: Buffer, nodeId: NodeId): Buffer { - return toBuffer( - getDiscv5Crypto().sha256(Buffer.concat([Buffer.from(ID_SIGNATURE_TEXT), challengeData, ephemPK, fromHex(nodeId)])) +export function generateIdSignatureInput(challengeData: Uint8Array, ephemPK: Uint8Array, nodeId: NodeId): Uint8Array { + return getDiscv5Crypto().sha256( + concatBytes(utf8ToBytes(ID_SIGNATURE_TEXT), challengeData, ephemPK, hexToBytes(nodeId)) ); } -export function decryptMessage(key: Buffer, nonce: Buffer, data: Buffer, aad: Buffer): Buffer { +export function decryptMessage(key: Uint8Array, nonce: Uint8Array, data: Uint8Array, aad: Uint8Array): Uint8Array { if (data.length < MAC_LENGTH) { throw new Error("message data not long enough"); } const ctx = Crypto.createDecipheriv("aes-128-gcm", key, nonce); ctx.setAAD(aad); ctx.setAuthTag(data.slice(data.length - MAC_LENGTH)); - return Buffer.concat([ + return concatBytes( ctx.update(data.slice(0, data.length - MAC_LENGTH)), // remove appended mac - ctx.final(), - ]); + ctx.final() + ); } -export function encryptMessage(key: Buffer, nonce: Buffer, data: Buffer, aad: Buffer): Buffer { +export function encryptMessage(key: Uint8Array, nonce: Uint8Array, data: Uint8Array, aad: Uint8Array): Uint8Array { const ctx = Crypto.createCipheriv("aes-128-gcm", key, nonce); ctx.setAAD(aad); - return Buffer.concat([ + return concatBytes( ctx.update(data), ctx.final(), - ctx.getAuthTag(), // append mac - ]); + ctx.getAuthTag() // append mac + ); } diff --git a/packages/discv5/src/session/service.ts b/packages/discv5/src/session/service.ts index 4d1c921d..dfb0c03f 100644 --- a/packages/discv5/src/session/service.ts +++ b/packages/discv5/src/session/service.ts @@ -3,7 +3,7 @@ import StrictEventEmitter from "strict-event-emitter-types"; import debug from "debug"; import { Multiaddr } from "@multiformats/multiaddr"; import { ENR, SignableENR } from "@chainsafe/enr"; - +import { bytesToHex, equalsBytes } from "ethereum-cryptography/utils.js"; import { IPMode, ITransportService } from "../transport/index.js"; import { PacketType, @@ -141,7 +141,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte super(); // ensure the keypair matches the one that signed the ENR - if (!keypair.publicKey.equals(enr.publicKey)) { + if (!equalsBytes(keypair.publicKey, enr.publicKey)) { throw new Error("Provided keypair does not match the provided ENR keypair"); } this.config = config; @@ -239,7 +239,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte this.send(nodeAddr, packet); - this.activeRequestsNonceMapping.set(packet.header.nonce.toString("hex"), nodeAddr); + this.activeRequestsNonceMapping.set(bytesToHex(packet.header.nonce), nodeAddr); this.activeRequests.set(nodeAddrStr, call); } @@ -272,7 +272,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte * This is called in response to a "whoAreYouRequest" event. * The application finds the highest known ENR for a node then we respond to the node with a WHOAREYOU packet. */ - public sendChallenge(nodeAddr: INodeAddress, nonce: Buffer, remoteEnr: ENR | null): void { + public sendChallenge(nodeAddr: INodeAddress, nonce: Uint8Array, remoteEnr: ENR | null): void { const nodeAddrStr = nodeAddressToString(nodeAddr); if (this.activeChallenges.peek(nodeAddrStr)) { @@ -322,7 +322,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte log("Cannot decode WHOAREYOU authdata from %s: %s", src, e); return; } - const nonce = packet.header.nonce.toString("hex"); + const nonce = bytesToHex(packet.header.nonce); // Check that this challenge matches a known active request. // If this message passes all the requisite checks, a request call is returned. @@ -368,7 +368,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte this.activeRequests.delete(nodeAddrStr); // double check the message nonces match - const requestNonce = requestCall.packet.header.nonce.toString("hex"); + const requestNonce = bytesToHex(requestCall.packet.header.nonce); if (requestNonce !== nonce) { // This could theoretically happen if a peer uses the same node id across // different connections. @@ -730,7 +730,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte } // Remove the associated nonce mapping. - this.activeRequestsNonceMapping.delete(requestCall.packet.header.nonce.toString("hex")); + this.activeRequestsNonceMapping.delete(bytesToHex(requestCall.packet.header.nonce)); // Remove the expected response this.removeExpectedResponse(nodeAddr.socketAddr); @@ -747,7 +747,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte private insertActiveRequest(requestCall: IRequestCall): void { const nodeAddr = getNodeAddress(requestCall.contact); const nodeAddrStr = nodeAddressToString(nodeAddr); - this.activeRequestsNonceMapping.set(requestCall.packet.header.nonce.toString("hex"), nodeAddr); + this.activeRequestsNonceMapping.set(bytesToHex(requestCall.packet.header.nonce), nodeAddr); this.activeRequests.set(nodeAddrStr, requestCall); } @@ -776,7 +776,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte log("Request timed out with %o", nodeAddr); // Remove the associated nonce mapping - this.activeRequestsNonceMapping.delete(requestCall.packet.header.nonce.toString("hex")); + this.activeRequestsNonceMapping.delete(bytesToHex(requestCall.packet.header.nonce)); this.removeExpectedResponse(nodeAddr.socketAddr); // The request has timed out. @@ -818,7 +818,7 @@ export class SessionService extends (EventEmitter as { new (): StrictEventEmitte private failRequest(requestCall: IRequestCall, error: RequestErrorType, removeSession: boolean): void { // The request has exported, remove the session. // Remove the associated nonce mapping. - this.activeRequestsNonceMapping.delete(requestCall.packet.header.nonce.toString("hex")); + this.activeRequestsNonceMapping.delete(bytesToHex(requestCall.packet.header.nonce)); // Fail the current request this.emit("requestFailed", requestCall.request.id, error); diff --git a/packages/discv5/src/session/session.ts b/packages/discv5/src/session/session.ts index 4b4e65ef..19365474 100644 --- a/packages/discv5/src/session/session.ts +++ b/packages/discv5/src/session/session.ts @@ -23,7 +23,6 @@ import { randomBytes } from "@noble/hashes/utils"; import { RequestId } from "../message/index.js"; import { IChallenge } from "."; import { getNodeId, getPublicKey, NodeContact } from "./nodeInfo.js"; -import { toBuffer } from "../util/toBuffer.js"; // The `Session` struct handles the stages of creating and establishing a handshake with a // peer. @@ -79,8 +78,8 @@ export class Session { localId: NodeId, remoteId: NodeId, challenge: IChallenge, - idSignature: Buffer, - ephPubkey: Buffer, + idSignature: Uint8Array, + ephPubkey: Uint8Array, enrRecord?: Uint8Array ): [Session, ENR] { let enr: ENR; @@ -133,8 +132,8 @@ export class Session { localKey: IKeypair, localNodeId: NodeId, updatedEnr: Uint8Array | null, - challengeData: Buffer, - message: Buffer + challengeData: Uint8Array, + message: Uint8Array ): [IPacket, Session] { // generate session keys const [encryptionKey, decryptionKey, ephPubkey] = generateSessionKeys( @@ -159,7 +158,7 @@ export class Session { }); const header = createHeader(PacketType.Handshake, authdata); - const maskingIv = toBuffer(randomBytes(MASKING_IV_SIZE)); + const maskingIv = randomBytes(MASKING_IV_SIZE); const aad = encodeChallengeData(maskingIv, header); // encrypt the message @@ -188,10 +187,10 @@ export class Session { * Encrypt packets with the current session key if we are awaiting a response from an * IAuthMessagePacket. */ - encryptMessage(srcId: NodeId, destId: NodeId, message: Buffer): IPacket { + encryptMessage(srcId: NodeId, destId: NodeId, message: Uint8Array): IPacket { const authdata = encodeMessageAuthdata({ srcId }); const header = createHeader(PacketType.Message, authdata); - const maskingIv = toBuffer(randomBytes(MASKING_IV_SIZE)); + const maskingIv = randomBytes(MASKING_IV_SIZE); const aad = encodeChallengeData(maskingIv, header); const ciphertext = encryptMessage(this.keys.encryptionKey, header.nonce, message, aad); return { @@ -207,7 +206,7 @@ export class Session { * upon failure, the new keys are attempted. If the new keys succeed, * the session keys are updated along with the Session state. */ - decryptMessage(nonce: Buffer, message: Buffer, aad: Buffer): Buffer { + decryptMessage(nonce: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array { // try with the new keys if (this.awaitingKeys) { const newKeys = this.awaitingKeys; diff --git a/packages/discv5/src/session/types.ts b/packages/discv5/src/session/types.ts index 9258f748..c91c7b28 100644 --- a/packages/discv5/src/session/types.ts +++ b/packages/discv5/src/session/types.ts @@ -66,8 +66,8 @@ export enum ResponseErrorType { InternalError, } export interface IKeys { - encryptionKey: Buffer; - decryptionKey: Buffer; + encryptionKey: Uint8Array; + decryptionKey: Uint8Array; } /** How we connected to the node. */ @@ -81,7 +81,7 @@ export enum ConnectionDirection { /** A Challenge (WHOAREYOU) object used to handle and send WHOAREYOU requests. */ export interface IChallenge { /** The challenge data received from the node. */ - data: Buffer; // length 63 + data: Uint8Array; // length 63 /** The remote's ENR if we know it. We can receive a challenge from an unknown node. */ remoteEnr?: ENR; } @@ -148,7 +148,7 @@ export interface ISessionEvents { * A WHOAREYOU packet needs to be sent. * This requests the protocol layer to send back the highest known ENR. */ - whoAreYouRequest: (nodeAddr: INodeAddress, nonce: Buffer) => void; + whoAreYouRequest: (nodeAddr: INodeAddress, nonce: Uint8Array) => void; /** * An RPC request failed. */ diff --git a/packages/discv5/src/transport/udp.ts b/packages/discv5/src/transport/udp.ts index 5d9f05b2..253bb4ed 100644 --- a/packages/discv5/src/transport/udp.ts +++ b/packages/discv5/src/transport/udp.ts @@ -135,7 +135,7 @@ export class UDPTransportService return getSocketAddressOnENR(enr, this.ipMode); } - private handleIncoming = (data: Buffer, rinfo: IRemoteInfo): void => { + private handleIncoming = (data: Uint8Array, rinfo: IRemoteInfo): void => { if (this.rateLimiter && !this.rateLimiter.allowEncodedPacket(rinfo.address)) { return; } diff --git a/packages/discv5/src/util/hexString.ts b/packages/discv5/src/util/hexString.ts deleted file mode 100644 index cc33dee9..00000000 --- a/packages/discv5/src/util/hexString.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function toHex(buf: Buffer): string { - return buf.toString("hex"); -} - -export function fromHex(str: string): Buffer { - return Buffer.from(str, "hex"); -} diff --git a/packages/discv5/src/util/index.ts b/packages/discv5/src/util/index.ts index 968afdec..a9c0a422 100644 --- a/packages/discv5/src/util/index.ts +++ b/packages/discv5/src/util/index.ts @@ -1,5 +1,4 @@ -export * from "./hexString.js"; export * from "./timeoutMap.js"; -export * from "./toBuffer.js"; +export * from "./toBytes.js"; export * from "./ip.js"; export * from "./error.js"; diff --git a/packages/discv5/src/util/toBuffer.ts b/packages/discv5/src/util/toBuffer.ts deleted file mode 100644 index 2677cbad..00000000 --- a/packages/discv5/src/util/toBuffer.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function toBuffer(arr: Uint8Array): Buffer { - if (arr instanceof Buffer) return arr; - return Buffer.from(arr.buffer, arr.byteOffset, arr.length); -} - -export function numberToBuffer(value: number, length: number): Buffer { - const res = Buffer.alloc(length); - res.writeUIntBE(value, 0, length); - return res; -} - -export function bufferToNumber(buffer: Buffer, length: number, offset = 0): number { - return buffer.readUIntBE(offset, length); -} diff --git a/packages/discv5/src/util/toBytes.ts b/packages/discv5/src/util/toBytes.ts new file mode 100644 index 00000000..b5dadd6c --- /dev/null +++ b/packages/discv5/src/util/toBytes.ts @@ -0,0 +1,15 @@ +export function numberToBytes(value: number, length: number): Uint8Array { + const array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[length - 1 - i] = (value >> (i * 8)) & 0xff; + } + return array; +} + +export function bytesToNumber(array: Uint8Array, length: number, offset = 0): number { + let value = 0; + for (let i = 0; i < length; i++) { + value = (value << 8) | array[offset + i]; + } + return value; +} diff --git a/packages/discv5/test/unit/kademlia/kademlia.test.ts b/packages/discv5/test/unit/kademlia/kademlia.test.ts index d28bd7fe..3f8e343f 100644 --- a/packages/discv5/test/unit/kademlia/kademlia.test.ts +++ b/packages/discv5/test/unit/kademlia/kademlia.test.ts @@ -4,7 +4,6 @@ import { expect } from "chai"; import { ENR, createNodeId, SignableENR } from "@chainsafe/enr"; import { EntryStatus, log2Distance } from "../../../src/kademlia/index.js"; import { randomBytes } from "@noble/hashes/utils"; -import { toBuffer } from "../../../src/util/index.js"; import { generateKeypair } from "../../../src/index.js"; describe("Kademlia routing table", () => { @@ -124,5 +123,5 @@ function randomENR(): ENR { } function randomNodeId(): string { - return createNodeId(toBuffer(randomBytes(32))); + return createNodeId(randomBytes(32)); } diff --git a/packages/discv5/test/unit/message/codec.test.ts b/packages/discv5/test/unit/message/codec.test.ts index 6249a0fe..91f9626a 100644 --- a/packages/discv5/test/unit/message/codec.test.ts +++ b/packages/discv5/test/unit/message/codec.test.ts @@ -1,11 +1,12 @@ import { expect } from "chai"; import { ENR } from "@chainsafe/enr"; import { Message, MessageType, decode, encode } from "../../../src/message/index.js"; +import { hexToBytes } from "ethereum-cryptography/utils.js"; describe("message", () => { const testCases: { message: Message; - expected: Buffer; + expected: Uint8Array; }[] = [ { message: { @@ -13,7 +14,7 @@ describe("message", () => { id: 1n, enrSeq: 1n, }, - expected: Buffer.from("01c20101", "hex"), + expected: hexToBytes("01c20101"), }, { message: { @@ -21,7 +22,7 @@ describe("message", () => { id: 1n, enrSeq: 0n, // < test 0 enrSeq }, - expected: Buffer.from("01c20100", "hex"), + expected: hexToBytes("01c20100"), }, { message: { @@ -30,7 +31,7 @@ describe("message", () => { enrSeq: 1n, addr: { ip: { type: 4, octets: new Uint8Array([127, 0, 0, 1]) }, port: 255 }, // 1 byte }, - expected: Buffer.from("02c90101847f00000181ff", "hex"), + expected: hexToBytes("02c90101847f00000181ff"), }, { message: { @@ -39,16 +40,16 @@ describe("message", () => { enrSeq: 1n, addr: { ip: { type: 4, octets: new Uint8Array([127, 0, 0, 1]) }, port: 5000 }, }, - expected: Buffer.from("02ca0101847f000001821388", "hex"), + expected: hexToBytes("02ca0101847f000001821388"), }, { message: { type: MessageType.PONG, id: 1n, enrSeq: 1n, - addr: { ip: { type: 6, octets: Buffer.alloc(16, 0xaa) }, port: 5000 }, // 2 bytes + addr: { ip: { type: 6, octets: new Uint8Array(16).fill(0xaa) }, port: 5000 }, // 2 bytes }, - expected: Buffer.from("02d6010190aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa821388", "hex"), + expected: hexToBytes("02d6010190aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa821388"), }, { message: { @@ -56,7 +57,7 @@ describe("message", () => { id: 1n, distances: [250], }, - expected: Buffer.from("03c401c281fa", "hex"), + expected: hexToBytes("03c401c281fa"), }, { message: { @@ -65,7 +66,7 @@ describe("message", () => { total: 1, enrs: [], }, - expected: Buffer.from("04c30101c0", "hex"), + expected: hexToBytes("04c30101c0"), }, { message: { @@ -81,9 +82,8 @@ describe("message", () => { ), ], }, - expected: Buffer.from( - "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235", - "hex" + expected: hexToBytes( + "04f8f20101f8eef875b8401ce2991c64993d7c84c29a00bdc871917551c7d330fca2dd0d69c706596dc655448f030b98a77d4001fd46ae0112ce26d613c5a6a02a81a6223cd0c4edaa53280182696482763489736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138f875b840d7f1c39e376297f81d7297758c64cb37dcc5c3beea9f57f7ce9695d7d5a67553417d719539d6ae4b445946de4d99e680eb8063f29485b555d45b7df16a1850130182696482763489736563703235366b31a1030e2cb74241c0c4fc8e8166f1a79a05d5b0dd95813a74b094529f317d5c39d235" ), }, ]; diff --git a/packages/discv5/test/unit/session/crypto.test.ts b/packages/discv5/test/unit/session/crypto.test.ts index d8e46827..01a58b9b 100644 --- a/packages/discv5/test/unit/session/crypto.test.ts +++ b/packages/discv5/test/unit/session/crypto.test.ts @@ -13,7 +13,6 @@ import { decryptMessage, } from "../../../src/session/index.js"; import { createKeypair, generateKeypair } from "../../../src/keypair/index.js"; -import { toBuffer } from "../../../src/index.js"; import { getDiscv5Crypto } from "../../../src/util/crypto.js"; describe("session crypto", () => { @@ -46,7 +45,7 @@ describe("session crypto", () => { "hex" ); - expect(deriveKey(toBuffer(secret), firstNodeId, secondNodeId, challengeData)).to.deep.equal(expected); + expect(deriveKey(secret, firstNodeId, secondNodeId, challengeData)).to.deep.equal(expected); }); it("symmetric keys should be derived correctly", () => { @@ -54,7 +53,7 @@ describe("session crypto", () => { const kp2 = generateKeypair("secp256k1"); const enr1 = SignableENR.createV4(kp1.privateKey); const enr2 = SignableENR.createV4(kp2.privateKey); - const nonce = toBuffer(randomBytes(32)); + const nonce = randomBytes(32); const [a1, b1, pk] = generateSessionKeys( enr1.nodeId, enr2.nodeId, @@ -106,10 +105,10 @@ describe("session crypto", () => { }); it("encrypted data should successfully be decrypted", () => { - const key = toBuffer(randomBytes(16)); - const nonce = toBuffer(randomBytes(12)); - const msg = toBuffer(randomBytes(16)); - const ad = toBuffer(randomBytes(16)); + const key = randomBytes(16); + const nonce = randomBytes(12); + const msg = randomBytes(16); + const ad = randomBytes(16); const cipher = encryptMessage(key, nonce, msg, ad); const decrypted = decryptMessage(key, nonce, cipher, ad); diff --git a/packages/discv5/test/unit/session/service.test.ts b/packages/discv5/test/unit/session/service.test.ts index 6016a361..b0a802e5 100644 --- a/packages/discv5/test/unit/session/service.test.ts +++ b/packages/discv5/test/unit/session/service.test.ts @@ -10,17 +10,18 @@ import { SessionService } from "../../../src/session/index.js"; import { createFindNodeMessage } from "../../../src/message/index.js"; import { defaultConfig } from "../../../src/config/index.js"; import { createNodeContact } from "../../../src/session/nodeInfo.js"; +import { hexToBytes } from "ethereum-cryptography/utils.js"; describe("session service", () => { const kp0 = createKeypair({ type: "secp256k1", - privateKey: Buffer.from("a93bedf04784c937059557c9dcb328f5f59fdb6e89295c30e918579250b7b01f", "hex"), - publicKey: Buffer.from("022663242e1092ea19e6bb41d67aa69850541a623b94bbea840ddceaab39789894", "hex"), + privateKey: hexToBytes("a93bedf04784c937059557c9dcb328f5f59fdb6e89295c30e918579250b7b01f"), + publicKey: hexToBytes("022663242e1092ea19e6bb41d67aa69850541a623b94bbea840ddceaab39789894"), }); const kp1 = createKeypair({ type: "secp256k1", - privateKey: Buffer.from("bd04e55f2a1424a4e69e96aad41cf763d2468d4358472e9f851569bdf47fb24c", "hex"), - publicKey: Buffer.from("03eae9945b354e9212566bc3f2740f3a62b3e1eb227dbed809f6dc2d3ea848c82e", "hex"), + privateKey: hexToBytes("bd04e55f2a1424a4e69e96aad41cf763d2468d4358472e9f851569bdf47fb24c"), + publicKey: hexToBytes("03eae9945b354e9212566bc3f2740f3a62b3e1eb227dbed809f6dc2d3ea848c82e"), }); const addr0 = multiaddr("/ip4/127.0.0.1/udp/49020"); diff --git a/packages/discv5/test/unit/transport/udp.test.ts b/packages/discv5/test/unit/transport/udp.test.ts index 1dd7e72e..9de3cf64 100644 --- a/packages/discv5/test/unit/transport/udp.test.ts +++ b/packages/discv5/test/unit/transport/udp.test.ts @@ -4,16 +4,16 @@ import { Multiaddr, multiaddr } from "@multiformats/multiaddr"; import { PacketType, IPacket, NONCE_SIZE, MASKING_IV_SIZE } from "../../../src/packet/index.js"; import { UDPTransportService } from "../../../src/transport/index.js"; -import { toHex } from "../../../src/util/index.js"; +import { bytesToHex } from "ethereum-cryptography/utils.js"; describe("UDP4 transport", () => { const address = "127.0.0.1"; - const nodeIdA = toHex(Buffer.alloc(32, 1)); + const nodeIdA = bytesToHex(new Uint8Array(32).fill(1)); const portA = 49523; const multiaddrA = multiaddr(`/ip4/${address}/udp/${portA}`); const a = new UDPTransportService({ bindAddrs: { ip4: multiaddrA }, nodeId: nodeIdA }); - const nodeIdB = toHex(Buffer.alloc(32, 2)); + const nodeIdB = bytesToHex(new Uint8Array(32).fill(2)); const portB = portA + 1; const multiaddrB = multiaddr(`/ip4/${address}/udp/${portB}`); const b = new UDPTransportService({ bindAddrs: { ip4: multiaddrB }, nodeId: nodeIdB }); @@ -30,16 +30,16 @@ describe("UDP4 transport", () => { it("should send and receive messages", async () => { const messagePacket: IPacket = { - maskingIv: Buffer.alloc(MASKING_IV_SIZE), + maskingIv: new Uint8Array(MASKING_IV_SIZE), header: { protocolId: "discv5", version: 1, flag: PacketType.Message, - nonce: Buffer.alloc(NONCE_SIZE), + nonce: new Uint8Array(NONCE_SIZE), authdataSize: 32, - authdata: Buffer.alloc(32, 2), + authdata: new Uint8Array(32).fill(2), }, - message: Buffer.alloc(44, 1), + message: new Uint8Array(44).fill(1), }; const received = new Promise<[Multiaddr, IPacket]>((resolve) => a.once("packet", (sender, packet) => resolve([sender, packet])) @@ -55,12 +55,12 @@ describe("UDP4 transport", () => { describe("UDP6 transport", () => { const address = "::1"; - const nodeIdA = toHex(Buffer.alloc(32, 1)); + const nodeIdA = bytesToHex(new Uint8Array(32).fill(1)); const portA = 49523; const multiaddrA = multiaddr(`/ip6/${address}/udp/${portA}`); const a = new UDPTransportService({ bindAddrs: { ip6: multiaddrA }, nodeId: nodeIdA }); - const nodeIdB = toHex(Buffer.alloc(32, 2)); + const nodeIdB = bytesToHex(new Uint8Array(32).fill(2)); const portB = portA + 1; const multiaddrB = multiaddr(`/ip6/${address}/udp/${portB}`); const b = new UDPTransportService({ bindAddrs: { ip6: multiaddrB }, nodeId: nodeIdB }); @@ -77,16 +77,16 @@ describe("UDP6 transport", () => { it("should send and receive messages", async () => { const messagePacket: IPacket = { - maskingIv: Buffer.alloc(MASKING_IV_SIZE), + maskingIv: new Uint8Array(MASKING_IV_SIZE), header: { protocolId: "discv5", version: 1, flag: PacketType.Message, - nonce: Buffer.alloc(NONCE_SIZE), + nonce: new Uint8Array(NONCE_SIZE), authdataSize: 32, - authdata: Buffer.alloc(32, 2), + authdata: new Uint8Array(32).fill(2), }, - message: Buffer.alloc(44, 1), + message: new Uint8Array(44).fill(1), }; const received = new Promise<[Multiaddr, IPacket]>((resolve) => a.once("packet", (sender, packet) => resolve([sender, packet])) @@ -103,13 +103,13 @@ describe("UDP6 transport", () => { describe("UDP4+6 transport", () => { const address4 = "127.0.0.1"; const address6 = "::1"; - const nodeIdA = toHex(Buffer.alloc(32, 1)); + const nodeIdA = bytesToHex(new Uint8Array(32).fill(1)); const portA = 49523; const multiaddr4A = multiaddr(`/ip4/${address4}/udp/${portA}`); const multiaddr6A = multiaddr(`/ip6/${address6}/udp/${portA + 1}`); const a = new UDPTransportService({ bindAddrs: { ip4: multiaddr4A, ip6: multiaddr6A }, nodeId: nodeIdA }); - const nodeIdB = toHex(Buffer.alloc(32, 2)); + const nodeIdB = bytesToHex(new Uint8Array(32).fill(2)); const portB = portA + 1; const multiaddr4B = multiaddr(`/ip4/${address4}/udp/${portB}`); const multiaddr6B = multiaddr(`/ip6/${address6}/udp/${portB + 1}`); @@ -127,16 +127,16 @@ describe("UDP4+6 transport", () => { it("should send and receive messages", async () => { const messagePacket: IPacket = { - maskingIv: Buffer.alloc(MASKING_IV_SIZE), + maskingIv: new Uint8Array(MASKING_IV_SIZE), header: { protocolId: "discv5", version: 1, flag: PacketType.Message, - nonce: Buffer.alloc(NONCE_SIZE), + nonce: new Uint8Array(NONCE_SIZE), authdataSize: 32, - authdata: Buffer.alloc(32, 2), + authdata: new Uint8Array(32).fill(2), }, - message: Buffer.alloc(44, 1), + message: new Uint8Array(44).fill(1), }; async function send(multiaddr: Multiaddr, nodeId: string, packet: IPacket): Promise<[Multiaddr, IPacket]> { const received = new Promise<[Multiaddr, IPacket]>((resolve) => diff --git a/packages/enr/package.json b/packages/enr/package.json index b71edad4..33226884 100644 --- a/packages/enr/package.json +++ b/packages/enr/package.json @@ -53,14 +53,13 @@ "@types/bn.js": "^4.11.5" }, "dependencies": { + "@ethereumjs/rlp": "^5.0.2", "@libp2p/crypto": "^5.0.1", "@libp2p/interface": "^2.0.1", "@libp2p/peer-id": "^5.0.1", "@multiformats/multiaddr": "^12.1.10", - "bigint-buffer": "^1.1.5", + "@scure/base": "^1.2.1", "ethereum-cryptography": "^2.2.0", - "rlp": "^2.2.6", - "uint8-varint": "^2.0.2", - "uint8arrays": "^5.0.1" + "uint8-varint": "^2.0.2" } } diff --git a/packages/enr/src/enr.ts b/packages/enr/src/enr.ts index 1ef9dc5f..00952636 100644 --- a/packages/enr/src/enr.ts +++ b/packages/enr/src/enr.ts @@ -1,5 +1,5 @@ import { Multiaddr, multiaddr, protocols } from "@multiformats/multiaddr"; -import * as RLP from "rlp"; +import * as RLP from "@ethereumjs/rlp"; import { KeyType, PeerId, PrivateKey } from "@libp2p/interface"; import { convertToString, convertToBytes } from "@multiformats/multiaddr/convert"; import { encode as varintEncode } from "uint8-varint"; @@ -7,9 +7,9 @@ import { encode as varintEncode } from "uint8-varint"; import { ERR_INVALID_ID, MAX_RECORD_SIZE } from "./constants.js"; import { ENRKey, ENRValue, SequenceNumber, NodeId } from "./types.js"; import { createPeerIdFromPublicKey } from "./peerId.js"; -import { fromBase64url, toBase64url, toBigInt, toNewUint8Array } from "./util.js"; +import { bytesToBigint, fromBase64url, toBase64url, toNewUint8Array } from "./util.js"; import { getV4Crypto } from "./crypto.js"; -import { compare, fromString, toString } from "uint8arrays"; +import { bytesToUtf8, equalsBytes, utf8ToBytes } from "ethereum-cryptography/utils.js"; /** ENR identity scheme */ export enum IDScheme { @@ -33,7 +33,7 @@ export type SignableENRData = { export function id(kvs: ReadonlyMap): IDScheme { const idBuf = kvs.get("id"); if (!idBuf) throw new Error("id not found"); - const id = toString(idBuf, "utf8") as IDScheme; + const id = bytesToUtf8(idBuf) as IDScheme; if (IDScheme[id] == null) { throw new Error("Unknown enr id scheme: " + id); } @@ -133,16 +133,17 @@ export function decodeFromValues(decoded: Uint8Array[]): ENRData { for (let i = 2; i < decoded.length; i += 2) { const k = decoded[i]; const v = decoded[i + 1]; - kvs.set(k.toString(), v); + kvs.set(bytesToUtf8(k), v); signed.push(k, v); } + const _id = id(kvs); if (!verify(_id, RLP.encode(signed), publicKey(_id, kvs), signature)) { throw new Error("Unable to verify enr signature"); } return { kvs, - seq: toBigInt(seq), + seq: bytesToBigint(seq), signature, }; } @@ -467,7 +468,7 @@ export class SignableENR extends BaseENR { this._signature = signature; if (this.id === IDScheme.v4) { - if (compare(getV4Crypto().publicKey(this.privateKey), this.publicKey) !== 0) { + if (!equalsBytes(getV4Crypto().publicKey(this.privateKey), this.publicKey)) { throw new Error("Provided keypair doesn't match kv pubkey"); } } @@ -481,7 +482,7 @@ export class SignableENR extends BaseENR { return new SignableENR( { ...kvs, - id: fromString("v4"), + id: utf8ToBytes("v4"), secp256k1: getV4Crypto().publicKey(privateKey), }, BigInt(1), diff --git a/packages/enr/src/index.ts b/packages/enr/src/index.ts index 4a9aefec..95e32748 100644 --- a/packages/enr/src/index.ts +++ b/packages/enr/src/index.ts @@ -4,4 +4,4 @@ export * as defaultCrypto from "./defaultCrypto.js"; export * from "./enr.js"; export * from "./types.js"; export * from "./peerId.js"; -export { createNodeId } from "./util.js"; +export * from "./util.js"; diff --git a/packages/enr/src/util.ts b/packages/enr/src/util.ts index 3db03f91..4fa5129a 100644 --- a/packages/enr/src/util.ts +++ b/packages/enr/src/util.ts @@ -1,7 +1,6 @@ -import { toBigIntBE } from "bigint-buffer"; -import { fromString, toString } from "uint8arrays"; import { NodeId } from "./types.js"; - +import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils"; +import { base64urlnopad } from "@scure/base"; // multiaddr 8.0.0 expects an Uint8Array with internal buffer starting at 0 offset export function toNewUint8Array(buf: Uint8Array): Uint8Array { const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); @@ -9,29 +8,11 @@ export function toNewUint8Array(buf: Uint8Array): Uint8Array { } export function toBase64url(buf: Uint8Array): string { - if (globalThis.Buffer != null) { - return globalThis.Buffer.from(buf).toString("base64url"); - } - return toString(buf, "base64url"); + return base64urlnopad.encode(buf); } export function fromBase64url(str: string): Uint8Array { - if (globalThis.Buffer != null) { - return globalThis.Buffer.from(str, "base64url"); - } - return fromString(str, "base64url"); -} - -export function toBigInt(buf: Uint8Array): bigint { - if (globalThis.Buffer != null) { - return toBigIntBE(globalThis.Buffer.from(buf)); - } - - if (buf.length === 0) { - return BigInt(0); - } - - return BigInt(`0x${toString(buf, "hex")}`); + return base64urlnopad.decode(str); } export function createNodeId(buf: Uint8Array): NodeId { @@ -39,9 +20,17 @@ export function createNodeId(buf: Uint8Array): NodeId { throw new Error("NodeId must be 32 bytes in length"); } - if (globalThis.Buffer != null) { - return globalThis.Buffer.from(buf).toString("hex"); + return bytesToHex(buf); +} + +export function bigintToBytes(n: bigint): Uint8Array { + let hex = n.toString(16); + if (hex.length % 2 !== 0) { + hex = `0${hex}`; } + return hexToBytes(hex); +} - return toString(buf, "hex"); +export function bytesToBigint(bytes: Uint8Array): bigint { + return BigInt(`0x${bytesToHex(bytes)}`); } diff --git a/packages/enr/test/unit/enr.test.ts b/packages/enr/test/unit/enr.test.ts index 23657ffc..ca3890ba 100644 --- a/packages/enr/test/unit/enr.test.ts +++ b/packages/enr/test/unit/enr.test.ts @@ -4,26 +4,24 @@ import { generateKeyPair } from "@libp2p/crypto/keys"; import { multiaddr } from "@multiformats/multiaddr"; import { BaseENR, ENR, SignableENR, getV4Crypto } from "../../src/index.js"; import { peerIdFromString } from "@libp2p/peer-id"; - -const toHex = (buf: Uint8Array): string => Buffer.from(buf).toString("hex"); +import { bytesToHex, hexToBytes, utf8ToBytes } from "ethereum-cryptography/utils.js"; describe("ENR spec test vector", () => { // spec enr https://eips.ethereum.org/EIPS/eip-778 - const privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); + const privateKey = hexToBytes("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); const publicKey = getV4Crypto().publicKey(privateKey); const text = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; const seq = BigInt(1); - const signature = Buffer.from( - "7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c", - "hex" + const signature = hexToBytes( + "7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c" ); const kvs = new Map( Object.entries({ - id: Buffer.from("v4"), + id: utf8ToBytes("v4"), secp256k1: publicKey, - ip: Buffer.from("7f000001", "hex"), - udp: Buffer.from((30303).toString(16), "hex"), + ip: hexToBytes("7f000001"), + udp: hexToBytes((30303).toString(16)), }) ); const nodeId = "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"; @@ -41,7 +39,7 @@ describe("ENR spec test vector", () => { }); it("should properly create and encode", () => { - expect(new SignableENR(kvs, seq, privateKey).encodeTxt()).to.equal(text); + expect(new SignableENR(kvs, seq, privateKey, signature).encodeTxt()).to.equal(text); }); it("should properly compute the node id", () => { @@ -66,7 +64,7 @@ describe("ENR spec test vector", () => { }); describe("ENR multiaddr support", () => { - const privateKey = Buffer.from("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291", "hex"); + const privateKey = hexToBytes("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"); let record: SignableENR; beforeEach(() => { @@ -242,7 +240,7 @@ describe("ENR", function () { 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)); + expect(bytesToHex(enr2.signature as Uint8Array)).to.be.equal(bytesToHex(enr.signature as Uint8Array)); const mu = enr2.getLocationMultiaddr("udp")!; expect(mu.toString()).to.be.equal("/ip4/18.223.219.100/udp/9000"); }); @@ -251,9 +249,9 @@ describe("ENR", function () { const txt = "enr:-Ku4QMh15cIjmnq-co5S3tYaNXxDzKTgj0ufusA-QfZ66EWHNsULt2kb0eTHoo1Dkjvvf6CAHDS1Di-htjiPFZzaIPcLh2F0dG5ldHOIAAAAAAAAAACEZXRoMpD2d10HAAABE________x8AgmlkgnY0gmlwhHZFkMSJc2VjcDI1NmsxoQIWSDEWdHwdEA3Lw2B_byeFQOINTZ0GdtF9DBjes6JqtIN1ZHCCIyg"; const enr = ENR.decodeTxt(txt); - const eth2 = enr.kvs.get("eth2") as Buffer; + const eth2 = enr.kvs.get("eth2") as Uint8Array; expect(eth2).to.not.be.undefined; - expect(toHex(eth2)).to.be.equal("f6775d0700000113ffffffffffff1f00"); + expect(bytesToHex(eth2)).to.be.equal("f6775d0700000113ffffffffffff1f00"); }); it("should encodeTxt and decodeTxt ipv6 enr successfully", async () => { @@ -271,7 +269,7 @@ describe("ENR", function () { ENR.decodeTxt(txt); expect.fail("Expect error here"); } catch (err: any) { - expect(err.message).to.be.equal("id not found"); + expect(err.message).to.be.equal("invalid RLP: encoded list too short"); } }); @@ -295,7 +293,7 @@ describe("ENR fuzzing testcases", () => { try { ENR.decodeTxt(txt); } catch (e: any) { - expect(e.message).to.equal("Decoded ENR invalid signature: must be a byte array"); + expect(e.message).to.equal("invalid RLP (safeSlice): end slice of Uint8Array out-of-bounds"); } }); it("should throw error in invalid sequence number", () => { @@ -304,7 +302,7 @@ describe("ENR fuzzing testcases", () => { try { ENR.decodeTxt(txt); } catch (e: any) { - expect(e.message).to.equal("Decoded ENR invalid sequence number: must be a byte array"); + expect(e.message).to.equal("invalid RLP (safeSlice): end slice of Uint8Array out-of-bounds"); } }); }); diff --git a/yarn.lock b/yarn.lock index c465cea9..cf1ddddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -359,6 +359,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@ethereumjs/rlp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" + integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== + "@fastify/busboy@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff" @@ -1019,6 +1024,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@scure/base@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" + integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== + "@scure/base@~1.1.6": version "1.1.8" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.8.tgz#8f23646c352f020c83bca750a82789e246d42b50" @@ -1652,25 +1662,11 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" -bigint-buffer@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" - integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA== - dependencies: - bindings "^1.3.0" - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bindings@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1690,11 +1686,6 @@ bluebird@^3.3.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - body-parser@^1.16.1: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -2936,11 +2927,6 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - filelist@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -5824,13 +5810,6 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" -rlp@^2.2.6: - version "2.2.7" - resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" - integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== - dependencies: - bn.js "^5.2.0" - run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"