From 05c70c2daf57e68fdeee0ba5038749509b4dc564 Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:33:38 -0700 Subject: [PATCH 1/7] create ScoredPeer class --- packages/portalnetwork/src/networks/peers.ts | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/portalnetwork/src/networks/peers.ts diff --git a/packages/portalnetwork/src/networks/peers.ts b/packages/portalnetwork/src/networks/peers.ts new file mode 100644 index 000000000..2aedbdfb2 --- /dev/null +++ b/packages/portalnetwork/src/networks/peers.ts @@ -0,0 +1,49 @@ +import type { ENR } from '@chainsafe/enr' +import type { INodeAddress } from '../client/types.js' +import type { IClientInfo } from '../index.js' + +export type PeerScore = { + errors: number +} + +export class ScoredPeer { + nodeAddress: INodeAddress + enr?: ENR + score: PeerScore = { + errors: 0, + } + capabilities: Array = [] + clientInfo?: IClientInfo + radius?: bigint + ignored: boolean = false + banned: boolean = false + constructor({ + nodeAddress, + enr, + score, + capabilities, + clientInfo, + radius, + ignored, + banned, + }: { + nodeAddress: INodeAddress + enr?: ENR + score?: PeerScore + capabilities?: Array + clientInfo?: IClientInfo + radius?: bigint + ignored?: boolean + banned?: boolean + }) { + this.nodeAddress = nodeAddress + this.enr = enr + this.score = score ?? this.score + this.capabilities = capabilities ?? this.capabilities + this.clientInfo = clientInfo + this.radius = radius + this.ignored = ignored ?? this.ignored + this.banned = banned ?? this.banned + } +} + From 8971233e7dc89114c39fba3750f32e527d9ea38c Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:34:06 -0700 Subject: [PATCH 2/7] add ScoredPeer map to network routing table --- packages/portalnetwork/src/client/routingTable.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/portalnetwork/src/client/routingTable.ts b/packages/portalnetwork/src/client/routingTable.ts index 52231e8c5..af0c391f4 100644 --- a/packages/portalnetwork/src/client/routingTable.ts +++ b/packages/portalnetwork/src/client/routingTable.ts @@ -3,16 +3,22 @@ import { KademliaRoutingTable } from '@chainsafe/discv5' import type { ENR, NodeId } from '@chainsafe/enr' import type { Debugger } from 'debug' import { shortId } from '../index.js' +import { ScoredPeer } from '../networks/peers.js' export class PortalNetworkRoutingTable extends KademliaRoutingTable { public logger?: Debugger private radiusMap: Map private gossipMap: Map> private ignored: [number, NodeId][] + /** + * Map of all known peers in the network + */ + private networkCensus: Map constructor(nodeId: NodeId) { super(nodeId) this.radiusMap = new Map() this.gossipMap = new Map() this.ignored = [] + this.networkCensus = new Map() } public setLogger(logger: Debugger) { From 103909ff26d8b6e78733143579bdd94e1a48954e Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:36:57 -0700 Subject: [PATCH 3/7] add methods for updating peer info from ping/pong --- .../portalnetwork/src/client/routingTable.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/packages/portalnetwork/src/client/routingTable.ts b/packages/portalnetwork/src/client/routingTable.ts index af0c391f4..ec1ef78b7 100644 --- a/packages/portalnetwork/src/client/routingTable.ts +++ b/packages/portalnetwork/src/client/routingTable.ts @@ -2,6 +2,7 @@ import { KademliaRoutingTable } from '@chainsafe/discv5' import type { ENR, NodeId } from '@chainsafe/enr' import type { Debugger } from 'debug' +import type { IClientInfo, INodeAddress } from '../index.js' import { shortId } from '../index.js' import { ScoredPeer } from '../networks/peers.js' export class PortalNetworkRoutingTable extends KademliaRoutingTable { @@ -109,4 +110,80 @@ export class PortalNetworkRoutingTable extends KademliaRoutingTable { before - this.ignored.length > 0 && this.logger!(`${before - this.ignored.length} nodeId's are no longer ignored`) } + + public nodeInCensus = (nodeId: NodeId) => { + return this.networkCensus.has(nodeId) + } + + public updateNodeFromPing = ( + nodeAddress: INodeAddress, + { + capabilities, + clientInfo, + radius, + }: { + radius: bigint + capabilities?: number[] + clientInfo?: IClientInfo + }, + ) => { + this.updateCensusFromNodeAddress(nodeAddress) + this.updateRadius(nodeAddress.nodeId, radius) + const peer = this.networkCensus.get(nodeAddress.nodeId)! + peer.radius = radius + if (capabilities !== undefined) { + peer.capabilities = capabilities + } + if (clientInfo !== undefined) { + peer.clientInfo = clientInfo + } + } + + public updateNodeFromPong = ( + enr: ENR, + { + capabilities, + clientInfo, + radius, + }: { + radius: bigint + capabilities?: number[] + clientInfo?: IClientInfo + }, + ) => { + this.updateCensusFromENR(enr) + this.updateRadius(enr.nodeId, radius) + const peer = this.networkCensus.get(enr.nodeId)! + peer.enr = enr + peer.radius = radius + if (capabilities !== undefined) { + peer.capabilities = capabilities + } + if (clientInfo !== undefined) { + peer.clientInfo = clientInfo + } + + } + + public updateCensusFromENR = (enr: ENR) => { + const peer = this.networkCensus.get(enr.nodeId) + if (peer) { + peer.enr = enr + } else { + const nodeAddress: INodeAddress = { + nodeId: enr.nodeId, + socketAddr: enr.getLocationMultiaddr('udp')!, + } + this.networkCensus.set(enr.nodeId, new ScoredPeer({ nodeAddress, enr })) + } + } + + public updateCensusFromNodeAddress = (nodeAddress: INodeAddress) => { + const peer = this.networkCensus.get(nodeAddress.nodeId) + if (peer) { + peer.nodeAddress = nodeAddress + } else { + this.networkCensus.set(nodeAddress.nodeId, new ScoredPeer({ nodeAddress })) + } + } } From 837c2e7d24703ad0c5994102360f12d2890f1a39 Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:37:19 -0700 Subject: [PATCH 4/7] update peer info during ping/pong handling --- .../portalnetwork/src/networks/network.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/portalnetwork/src/networks/network.ts b/packages/portalnetwork/src/networks/network.ts index 4b558187c..ed18cbf24 100644 --- a/packages/portalnetwork/src/networks/network.ts +++ b/packages/portalnetwork/src/networks/network.ts @@ -327,17 +327,21 @@ export abstract class BaseNetwork extends EventEmitter { this.logger.extend('PONG')( `Client ${shortId(enr.nodeId)} is ${decodeClientInfo(ClientInfo).clientName} node with capabilities: ${Capabilities}`, ) - this.routingTable.updateRadius(enr.nodeId, DataRadius) + this.routingTable.updateNodeFromPong(enr, { + capabilities: Capabilities, + clientInfo: decodeClientInfo(ClientInfo), + radius: DataRadius, + }) break } case PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD: { const { dataRadius } = BasicRadius.deserialize(pongMessage.customPayload) - this.routingTable.updateRadius(enr.nodeId, dataRadius) + this.routingTable.updateNodeFromPong(enr, { radius: dataRadius }) break } case PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD: { const { dataRadius } = HistoryRadius.deserialize(pongMessage.customPayload) - this.routingTable.updateRadius(enr.nodeId, dataRadius) + this.routingTable.updateNodeFromPong(enr, { radius: dataRadius }) break } case PingPongPayloadExtensions.ERROR_RESPONSE: { @@ -377,20 +381,26 @@ export abstract class BaseNetwork extends EventEmitter { if (this.capabilities.includes(pingMessage.payloadType)) { switch (pingMessage.payloadType) { case PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES: { - const { DataRadius } = ClientInfoAndCapabilities.deserialize(pingMessage.customPayload) - this.routingTable.updateRadius(src.nodeId, DataRadius) + const { DataRadius, Capabilities, ClientInfo } = ClientInfoAndCapabilities.deserialize( + pingMessage.customPayload, + ) + this.routingTable.updateNodeFromPing(src, { + capabilities: Capabilities, + clientInfo: decodeClientInfo(ClientInfo), + radius: DataRadius, + }) pongPayload = this.pingPongPayload(pingMessage.payloadType) break } case PingPongPayloadExtensions.BASIC_RADIUS_PAYLOAD: { const { dataRadius } = BasicRadius.deserialize(pingMessage.customPayload) - this.routingTable.updateRadius(src.nodeId, dataRadius) + this.routingTable.updateNodeFromPing(src, { radius: dataRadius }) pongPayload = this.pingPongPayload(pingMessage.payloadType) break } case PingPongPayloadExtensions.HISTORY_RADIUS_PAYLOAD: { const { dataRadius } = HistoryRadius.deserialize(pingMessage.customPayload) - this.routingTable.updateRadius(src.nodeId, dataRadius) + this.routingTable.updateNodeFromPing(src, { radius: dataRadius }) pongPayload = this.pingPongPayload(pingMessage.payloadType) break } From ecd94793b486560589a70a95c7bc052bc4deeef0 Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:43:04 -0700 Subject: [PATCH 5/7] make networkCensus public attribute --- packages/portalnetwork/src/client/routingTable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/portalnetwork/src/client/routingTable.ts b/packages/portalnetwork/src/client/routingTable.ts index ec1ef78b7..e8f302176 100644 --- a/packages/portalnetwork/src/client/routingTable.ts +++ b/packages/portalnetwork/src/client/routingTable.ts @@ -13,7 +13,7 @@ export class PortalNetworkRoutingTable extends KademliaRoutingTable { /** * Map of all known peers in the network */ - private networkCensus: Map + public networkCensus: Map constructor(nodeId: NodeId) { super(nodeId) this.radiusMap = new Map() From 0ee56ecb834a91d5372196dedb58c618c5897e1e Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:43:23 -0700 Subject: [PATCH 6/7] check peer capabilities during PING --- packages/portalnetwork/src/networks/network.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/portalnetwork/src/networks/network.ts b/packages/portalnetwork/src/networks/network.ts index ed18cbf24..bf26f84ea 100644 --- a/packages/portalnetwork/src/networks/network.ts +++ b/packages/portalnetwork/src/networks/network.ts @@ -296,8 +296,9 @@ export abstract class BaseNetwork extends EventEmitter { this.logger(`Invalid ENR provided. PING aborted`) return } - if (!this.routingTable.getWithPending(enr.nodeId)?.value && extensionType !== 0) { - throw new Error(`First PING message must be type 0: CLIENT_INFO_RADIUS_AND_CAPABILITIES.`) + const peerCapabilities = this.routingTable.networkCensus.get(enr.nodeId)?.capabilities ?? [0] + if (!peerCapabilities.includes(extensionType)) { + throw new Error(`Peer is not know to support extension type: ${extensionType}`) } const timeout = setTimeout(() => { return undefined From c101612c1703ec10b2aa786237700bd410595672 Mon Sep 17 00:00:00 2001 From: ScottyPoi Date: Mon, 3 Feb 2025 16:48:12 -0700 Subject: [PATCH 7/7] update test --- .../test/integration/pingpong.spec.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/portalnetwork/test/integration/pingpong.spec.ts b/packages/portalnetwork/test/integration/pingpong.spec.ts index 53f562f7a..6814d7941 100644 --- a/packages/portalnetwork/test/integration/pingpong.spec.ts +++ b/packages/portalnetwork/test/integration/pingpong.spec.ts @@ -3,21 +3,21 @@ import { keys } from '@libp2p/crypto' import { multiaddr } from '@multiformats/multiaddr' import { hexToBytes } from 'ethereum-cryptography/utils' import { assert, describe, it } from 'vitest' -import type { HistoryNetwork, INodeAddress } from '../../src' +import type { HistoryNetwork } from '../../src' import { ClientInfoAndCapabilities, HistoryRadius, NetworkId, PingPongPayloadExtensions, PortalNetwork, - PortalWireMessageType, TransportLayer, } from '../../src/index.js' +import { ScoredPeer } from '../../src/networks/peers.js' const privateKeys = [ - '0x0a2700250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c12250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c1a2408021220aae0fff4ac28fdcdf14ee8ecb591c7f1bc78651206d86afe16479a63d9cb73bd', - '0x0a27002508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd743764122508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd7437641a2408021220c6eb3ae347433e8cfe7a0a195cc17fc8afcd478b9fb74be56d13bccc67813130', - ] + '0x0a2700250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c12250802122102273097673a2948af93317235d2f02ad9cf3b79a34eeb37720c5f19e09f11783c1a2408021220aae0fff4ac28fdcdf14ee8ecb591c7f1bc78651206d86afe16479a63d9cb73bd', + '0x0a27002508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd743764122508021221039909a8a7e81dbdc867480f0eeb7468189d1e7a1dd7ee8a13ee486c8cbd7437641a2408021220c6eb3ae347433e8cfe7a0a195cc17fc8afcd478b9fb74be56d13bccc67813130', +] const pk1 = keys.privateKeyFromProtobuf(hexToBytes(privateKeys[0]).slice(-36)) const enr1 = SignableENR.createFromPrivateKey(pk1) @@ -62,16 +62,16 @@ describe('PING/PONG', async () => { await network1.sendPing(network2?.enr!.toENR(), 1) assert.fail('should have failed') } catch (e) { - assert.equal( - e.message, - 'First PING message must be type 0: CLIENT_INFO_RADIUS_AND_CAPABILITIES.', - ) + assert.equal(e.message, 'Peer is not know to support extension type: 1') } }) it('should exchange type 0 PING/PONG', async () => { const pingpong = await network1.sendPing(network2?.enr!.toENR(), 0) assert.exists(pingpong, 'should have received a pong') - assert.equal(pingpong!.payloadType, PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES) + assert.equal( + pingpong!.payloadType, + PingPongPayloadExtensions.CLIENT_INFO_RADIUS_AND_CAPABILITIES, + ) const { DataRadius } = ClientInfoAndCapabilities.deserialize(pingpong!.customPayload) assert.equal(DataRadius, network2.nodeRadius) }) @@ -83,6 +83,18 @@ describe('PING/PONG', async () => { assert.equal(dataRadius, network2.nodeRadius) }) it('should receive error response from unsupported capability', async () => { + // falsely update peer's capabilities in network census + network1.routingTable.networkCensus.set( + network2.enr!.nodeId, + new ScoredPeer({ + nodeAddress: { + nodeId: network2.enr!.nodeId, + socketAddr: network2.enr!.getLocationMultiaddr('udp')!, + }, + capabilities: [1], + }), + ) + // send ping with unsupported capability const pingpong = await network1.sendPing(network2?.enr!.toENR(), 1) assert.exists(pingpong, 'should have received a pong') assert.equal(pingpong!.payloadType, PingPongPayloadExtensions.ERROR_RESPONSE)