diff --git a/packages/portalnetwork/src/networks/network.ts b/packages/portalnetwork/src/networks/network.ts index d50a90603..8f8124061 100644 --- a/packages/portalnetwork/src/networks/network.ts +++ b/packages/portalnetwork/src/networks/network.ts @@ -32,7 +32,6 @@ import { BasicRadius, ClientInfoAndCapabilities, ContentMessageType, - CustomPayloadExtensionsFormat, ErrorPayload, HistoryRadius, MAX_PACKET_SIZE, @@ -304,15 +303,12 @@ export abstract class BaseNetwork extends EventEmitter { return undefined }, 3000) try { - const customPayload = CustomPayloadExtensionsFormat.serialize({ - type: extensionType, - payload: this.pingPongPayload(extensionType), - }) const pingMsg = PortalWireMessageType.serialize({ selector: MessageCodes.PING, value: { enrSeq: this.enr.seq, - customPayload, + payloadType: extensionType, + customPayload: this.pingPongPayload(extensionType), }, }) this.logger.extend(`PING`)(`Sent to ${shortId(enr)}`) @@ -323,13 +319,11 @@ export abstract class BaseNetwork extends EventEmitter { const pongMessage = decoded.value as PongMessage // Received a PONG message so node is reachable, add to routing table this.updateRoutingTable(enr) - const { type, payload } = CustomPayloadExtensionsFormat.deserialize( - pongMessage.customPayload, - ) - switch (type) { + switch (pongMessage.payloadType) { case PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES: { - const { ClientInfo, Capabilities, DataRadius } = - ClientInfoAndCapabilities.deserialize(payload) + const { ClientInfo, Capabilities, DataRadius } = ClientInfoAndCapabilities.deserialize( + pongMessage.customPayload, + ) this.logger.extend('PONG')( `Client ${shortId(enr.nodeId)} is ${decodeClientInfo(ClientInfo).clientName} node with capabilities: ${Capabilities}`, ) @@ -337,17 +331,17 @@ export abstract class BaseNetwork extends EventEmitter { break } case PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD: { - const { dataRadius } = BasicRadius.deserialize(payload) + const { dataRadius } = BasicRadius.deserialize(pongMessage.customPayload) this.routingTable.updateRadius(enr.nodeId, dataRadius) break } case PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD: { - const { dataRadius } = HistoryRadius.deserialize(payload) + const { dataRadius } = HistoryRadius.deserialize(pongMessage.customPayload) this.routingTable.updateRadius(enr.nodeId, dataRadius) break } case PingPongPayloadExtensions.ERROR_RESPONSE: { - const { errorCode, message } = ErrorPayload.deserialize(payload) + const { errorCode, message } = ErrorPayload.deserialize(pongMessage.customPayload) this.logger.extend('PONG')( `Received error response from ${shortId(enr.nodeId)}: ${errorCode} - ${message}`, ) @@ -372,21 +366,19 @@ export abstract class BaseNetwork extends EventEmitter { } handlePing = async (src: INodeAddress, id: bigint, pingMessage: PingMessage) => { - const { type, payload } = CustomPayloadExtensionsFormat.deserialize(pingMessage.customPayload) if (!this.routingTable.getWithPending(src.nodeId)?.value) { - if (type !== PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES) { - const customPayload = CustomPayloadExtensionsFormat.serialize({ - type: PingPongPayloadExtensions.ERROR_RESPONSE, - payload: ErrorPayload.serialize({ - errorCode: PingPongErrorCodes.EXTENSION_NOT_SUPPORTED, - message: hexToBytes( - fromAscii( - `First PING message must be type 0: CLIENT_INFO_RADIUS_AND_CAPABILITIES. Received type ${type}`, - ), + if ( + pingMessage.payloadType !== PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES + ) { + const customPayload = ErrorPayload.serialize({ + errorCode: PingPongErrorCodes.EXTENSION_NOT_SUPPORTED, + message: hexToBytes( + fromAscii( + `First PING message must be type 0: CLIENT_INFO_RADIUS_AND_CAPABILITIES. Received type ${pingMessage.payloadType}`, ), - }), + ), }) - return this.sendPong(src, id, customPayload) + return this.sendPong(src, id, customPayload, PingPongPayloadExtensions.ERROR_RESPONSE) } // Check to see if node is already in corresponding network routing table and add if not const enr = this.findEnr(src.nodeId) @@ -395,31 +387,33 @@ export abstract class BaseNetwork extends EventEmitter { } } let pongPayload: Uint8Array - if (this.capabilities.includes(type)) { - switch (type) { + if (this.capabilities.includes(pingMessage.payloadType)) { + switch (pingMessage.payloadType) { case PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES: { - const { DataRadius } = ClientInfoAndCapabilities.deserialize(payload) + const { DataRadius } = ClientInfoAndCapabilities.deserialize(pingMessage.customPayload) this.routingTable.updateRadius(src.nodeId, DataRadius) - pongPayload = this.pingPongPayload(type) + pongPayload = this.pingPongPayload(pingMessage.payloadType) break } case PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD: { - const { dataRadius } = BasicRadius.deserialize(payload) + const { dataRadius } = BasicRadius.deserialize(pingMessage.customPayload) this.routingTable.updateRadius(src.nodeId, dataRadius) - pongPayload = this.pingPongPayload(type) + pongPayload = this.pingPongPayload(pingMessage.payloadType) break } case PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD: { - const { dataRadius } = HistoryRadius.deserialize(payload) + const { dataRadius } = HistoryRadius.deserialize(pingMessage.customPayload) this.routingTable.updateRadius(src.nodeId, dataRadius) - pongPayload = this.pingPongPayload(type) + pongPayload = this.pingPongPayload(pingMessage.payloadType) break } default: { pongPayload = ErrorPayload.serialize({ errorCode: 0, message: hexToBytes( - fromAscii(`${this.constructor.name} does not support PING extension type: ${type}`), + fromAscii( + `${this.constructor.name} does not support PING extension type: ${pingMessage.payloadType}`, + ), ), }) } @@ -428,28 +422,25 @@ export abstract class BaseNetwork extends EventEmitter { pongPayload = ErrorPayload.serialize({ errorCode: PingPongErrorCodes.EXTENSION_NOT_SUPPORTED, message: hexToBytes( - fromAscii(`${this.constructor.name} does not support PING extension type: ${type}`), + fromAscii( + `${this.constructor.name} does not support PING extension type: ${pingMessage.payloadType}`, + ), ), }) - return this.sendPong( - src, - id, - CustomPayloadExtensionsFormat.serialize({ - type: PingPongPayloadExtensions.ERROR_RESPONSE, - payload: pongPayload, - }), - ) + return this.sendPong(src, id, pongPayload, PingPongPayloadExtensions.ERROR_RESPONSE) } - const customPayload = CustomPayloadExtensionsFormat.serialize({ - type, - payload: pongPayload, - }) - return this.sendPong(src, id, customPayload) + return this.sendPong(src, id, pongPayload, pingMessage.payloadType) } - sendPong = async (src: INodeAddress, requestId: bigint, customPayload: Uint8Array) => { + sendPong = async ( + src: INodeAddress, + requestId: bigint, + customPayload: Uint8Array, + payloadType: number, + ) => { const payload = { enrSeq: this.enr.seq, + payloadType, customPayload, } const pongMsg = PortalWireMessageType.serialize({ diff --git a/packages/portalnetwork/src/wire/payloadExtensions.ts b/packages/portalnetwork/src/wire/payloadExtensions.ts index efd8d662f..d4ebc78a8 100644 --- a/packages/portalnetwork/src/wire/payloadExtensions.ts +++ b/packages/portalnetwork/src/wire/payloadExtensions.ts @@ -1,24 +1,9 @@ import { ByteListType, ContainerType, ListBasicType, UintBigintType, UintNumberType } from "@chainsafe/ssz"; import { bytesToHex, fromAscii, hexToBytes, toAscii } from "@ethereumjs/util"; +import { PingPongPayloadType } from "./types.js"; -/** - * Numeric identifier which tells clients how the payload field should be decoded. - */ -export const PingPongPayloadType = new UintNumberType(2) -/** - * SSZ encoded extension payload - */ -export const PingPongPayload = new ByteListType(1100) - -/** - * All payloads used in the Ping custom_payload MUST follow the Ping Custom Payload Extensions format. - */ -export const CustomPayloadExtensionsFormat = new ContainerType({ - type: PingPongPayloadType, - payload: PingPongPayload -}) /** @@ -53,13 +38,16 @@ export interface IClientInfo { export const MAX_CLIENT_INFO_BYTE_LENGTH = 200 +export function clientInfoStringToBytes(clientInfo: string): Uint8Array { + return hexToBytes(fromAscii(clientInfo)) +} /** * Encode Client info as ASCII hex encoded string. * @param clientInfo * @returns */ export function encodeClientInfo(clientInfo: IClientInfo): Uint8Array { - const clientInfoBytes = hexToBytes(fromAscii(Object.values(clientInfo).join("/"))) + const clientInfoBytes = clientInfoStringToBytes(Object.values(clientInfo).join("/")) if (clientInfoBytes.length > MAX_CLIENT_INFO_BYTE_LENGTH) { throw new Error(`Client info is too long: ${clientInfoBytes.length} > ${MAX_CLIENT_INFO_BYTE_LENGTH}`) } @@ -76,6 +64,7 @@ export function decodeClientInfo(clientInfo: Uint8Array): IClientInfo { }; } + export const ClientInfo = new ByteListType(MAX_CLIENT_INFO_BYTE_LENGTH) diff --git a/packages/portalnetwork/src/wire/types.ts b/packages/portalnetwork/src/wire/types.ts index 16d2cc971..729e8855c 100644 --- a/packages/portalnetwork/src/wire/types.ts +++ b/packages/portalnetwork/src/wire/types.ts @@ -36,24 +36,37 @@ export enum MessageCodes { export const ByteList = new ByteListType(2048) export const Bytes2 = new ByteVectorType(2) export const ENRs = new ListCompositeType(ByteList, 32) +/** + * Numeric identifier which tells clients how the payload field should be decoded. + */ +export const PingPongPayloadType = new UintNumberType(2) + +/** + * SSZ encoded extension payload + */ +export const PingPongPayload = new ByteListType(1100) export type PingMessage = { enrSeq: bigint + payloadType: number customPayload: PingPongCustomData } export type PongMessage = { enrSeq: bigint + payloadType: number customPayload: PingPongCustomData } export const PingMessageType = new ContainerType({ enrSeq: new UintBigintType(8), - customPayload: ByteList, + payloadType: PingPongPayloadType, + customPayload: PingPongPayload, }) export const PongMessageType = new ContainerType({ enrSeq: new UintBigintType(8), - customPayload: ByteList, + payloadType: PingPongPayloadType, + customPayload: PingPongPayload, }) export type FindNodesMessage = { diff --git a/packages/portalnetwork/test/wire/types.spec.ts b/packages/portalnetwork/test/wire/types.spec.ts index 859fc60c6..fc0578a3d 100644 --- a/packages/portalnetwork/test/wire/types.spec.ts +++ b/packages/portalnetwork/test/wire/types.spec.ts @@ -1,16 +1,202 @@ import { ENR } from '@chainsafe/enr' import { BitArray } from '@chainsafe/ssz' -import { bytesToHex, concatBytes, hexToBytes } from '@ethereumjs/util' +import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@ethereumjs/util' import { assert, describe, it } from 'vitest' import { ContentMessageType, MessageCodes, PortalWireMessageType } from '../../src/wire/types.js' import { BasicRadius, - CustomPayloadExtensionsFormat, + ClientInfoAndCapabilities, + ErrorPayload, + HistoryRadius, PingPongPayloadExtensions, + clientInfoStringToBytes, } from '../../src/wire/index.js' +describe('ping pong message encoding', () => { + it('should encode type 0 ping with client info', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + clientInfo: "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0", + capabilities: [0, 1, 65535] + } + const payload = ClientInfoAndCapabilities.serialize({ + ClientInfo: clientInfoStringToBytes(params.clientInfo), + DataRadius: params.dataRadius, + Capabilities: params.capabilities, + }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PING, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 0 ping without client info', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + clientInfo: "", + capabilities: [0, 1, 65535] + } + const payload = ClientInfoAndCapabilities.serialize({ + ClientInfo: clientInfoStringToBytes(params.clientInfo), + DataRadius: params.dataRadius, + Capabilities: params.capabilities, + }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PING, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 0 pong with client info', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + clientInfo: "trin/v0.1.1-b61fdc5c/linux-x86_64/rustc1.81.0", + capabilities: [0, 1, 65535] + } + const payload = ClientInfoAndCapabilities.serialize({ + ClientInfo: clientInfoStringToBytes(params.clientInfo), + DataRadius: params.dataRadius, + Capabilities: params.capabilities, + }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PONG, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 0 pong without client info', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + clientInfo: "", + capabilities: [0, 1, 65535] + } + const payload = ClientInfoAndCapabilities.serialize({ + ClientInfo: clientInfoStringToBytes(params.clientInfo), + DataRadius: params.dataRadius, + Capabilities: params.capabilities, + }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PONG, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 1 ping', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + } + const payload = BasicRadius.serialize({ dataRadius: params.dataRadius }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PING, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 1 pong', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + } + const payload = BasicRadius.serialize({ dataRadius: params.dataRadius }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PONG, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 2 ping', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + ephemeralHeaderCount: 4242 + } + const payload = HistoryRadius.serialize({ dataRadius: params.dataRadius, ephemeralHeadersCount: params.ephemeralHeaderCount }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PING, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 2 pong', () => { + const params = { + enrSeq: BigInt(1), + dataRadius: 2n ** 256n - 2n, + ephemeralHeaderCount: 4242 + } + const payload = HistoryRadius.serialize({ dataRadius: params.dataRadius, ephemeralHeadersCount: params.ephemeralHeaderCount }) + const pingMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PONG, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pingMessage), expectedOutput, 'ping message encoded correctly') + }) + it('should encode type 65535 pong', () => { + const params = { + enrSeq: BigInt(1), + errorCode: 2, + message: 'hello world' + } + const payload = ErrorPayload.serialize({ errorCode: params.errorCode, message: utf8ToBytes(params.message) }) + const pongMessage = PortalWireMessageType.serialize({ + selector: MessageCodes.PONG, + value: { + enrSeq: params.enrSeq, + payloadType: PingPongPayloadExtensions.ERROR_RESPONSE, + customPayload: payload, + }, + }) + const expectedOutput = '' + assert.equal(bytesToHex(pongMessage), expectedOutput, 'pong message encoded correctly') + }) +}) + describe('message encoding should match test vectors', () => { // Validate PING/PONG message encoding const enrSeq = BigInt(1) @@ -22,14 +208,12 @@ describe('message encoding should match test vectors', () => { selector: MessageCodes.PING, value: { enrSeq, - customPayload: CustomPayloadExtensionsFormat.serialize({ - type: PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD, - payload: BasicRadius.serialize({ dataRadius }), - }), + payloadType: PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD, + customPayload: BasicRadius.serialize({ dataRadius }), }, }) testVector = - '0x0001000000000000000c000000010006000000feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + '0x00010000000000000001000e000000feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' assert.equal(bytesToHex(payload), testVector, 'ping message encoded correctly') })