UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

960 lines (959 loc) 37.6 kB
import { EventEmitter } from 'events'; import { createLibp2p } from 'libp2p'; import { multiaddr } from '@multiformats/multiaddr'; import { isPrivate } from '@libp2p/utils'; import { webSockets } from '@libp2p/websockets'; import { noise } from '@chainsafe/libp2p-noise'; import { yamux } from '@libp2p/yamux'; import { kadDHT, passthroughMapper, removePrivateAddressesMapper, } from '@libp2p/kad-dht'; import { identify } from '@libp2p/identify'; import { ping } from '@libp2p/ping'; import { gossipsub } from '@libp2p/gossipsub'; import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'; import { circuitRelayServer } from '@libp2p/circuit-relay-v2'; import { autoNAT } from '@libp2p/autonat'; import { dcutr } from '@libp2p/dcutr'; import { uPnPNAT } from '@libp2p/upnp-nat'; import { bootstrap } from '@libp2p/bootstrap'; import { peerIdFromString } from '@libp2p/peer-id'; import { isBrowser } from '../../utils/functions.js'; import { ConnectionEvent, RelayEvent, } from './types.js'; import { P2PProtocol } from './protocol.js'; import { CoreSecurityManager } from './security.js'; export class P2PCoordinator extends EventEmitter { config; node; protocol; protocolHandlers = new Map(); seenMessages = new Set(); peerInfo = new Map(); dhtValues = new Map(); cleanupIntervalId; coreSecurityManager; lastAdvertisedMultiaddrs = []; topicHandlers = new Map(); constructor(config) { super(); this.config = config; this.protocol = new P2PProtocol(); this.coreSecurityManager = new CoreSecurityManager({ disableRateLimiting: config.securityConfig?.disableRateLimiting ?? false, customLimits: config.securityConfig?.customLimits, }); this.startDHTCleanup(); } getCoreSecurityManager() { return this.coreSecurityManager; } startDHTCleanup() { this.cleanupIntervalId = setInterval(() => { this.cleanup(); }, 5 * 60 * 1000); } async start() { let peerInfoMapper = this.config.dhtPeerInfoMapper; if (!peerInfoMapper) { if (isBrowser()) { peerInfoMapper = passthroughMapper; } else { const listenAddrs = this.config.listen || ['/ip4/0.0.0.0/tcp/0']; const isPrivateListenAddresses = listenAddrs.some(addr => isPrivate(multiaddr(addr))); if (isPrivateListenAddresses) { peerInfoMapper = passthroughMapper; } else { peerInfoMapper = removePrivateAddressesMapper; } } } const transports = []; if (isBrowser()) { transports.push(webSockets()); try { const { webRTC } = await import('@libp2p/webrtc'); transports.push(webRTC()); } catch { console.warn('WebRTC transport not available. Install @libp2p/webrtc for browser-to-browser P2P.'); } } else { const { tcp } = await import('@libp2p/tcp'); transports.push(tcp()); transports.push(webSockets()); } if (this.config.enableRelay !== false) { transports.push(circuitRelayTransport()); } const services = { identify: identify(), ping: ping(), }; if (this.config.enableDHT !== false) { services.kadDHT = kadDHT({ protocol: this.config.dhtProtocol || '/lotus/kad/1.0.0', clientMode: !(this.config.enableDHTServer ?? false), peerInfoMapper, }); } if (this.config.enableGossipSub !== false) { services.pubsub = gossipsub({ allowPublishToZeroTopicPeers: true, emitSelf: false, doPX: true, }); } if (this.config.enableRelayServer === true) { services.relay = circuitRelayServer({ reservations: { maxReservations: 100, }, }); } if (this.config.enableAutoNAT !== false) { services.autoNAT = autoNAT(); } if (this.config.enableDCUTR !== false && this.config.enableRelay !== false) { services.dcutr = dcutr(); } if (this.config.enableUPnP === true) { services.upnpNAT = uPnPNAT(); } const peerDiscovery = []; if (this.config.bootstrapPeers && this.config.bootstrapPeers.length > 0) { let bootstrapList = this.config.bootstrapPeers; if (isBrowser()) { bootstrapList = bootstrapList.filter(addr => { return (addr.includes('/ws') || addr.includes('/wss') || addr.includes('/webrtc')); }); if (bootstrapList.length === 0 && this.config.bootstrapPeers.length > 0) { console.warn('No browser-compatible bootstrap peers found. ' + 'Browsers require WebSocket (ws/wss) or WebRTC addresses. ' + 'TCP addresses are not supported in browsers.'); } } if (bootstrapList.length > 0) { console.log(`[P2P] Bootstrap peers (${isBrowser() ? 'browser' : 'node'}):`, bootstrapList); peerDiscovery.push(bootstrap({ list: bootstrapList, })); } } let listenAddrs; if (isBrowser()) { listenAddrs = this.config.listen ? [...this.config.listen] : ['/p2p-circuit']; } else { listenAddrs = this.config.listen ? [...this.config.listen] : ['/ip4/0.0.0.0/tcp/0']; } if (this.config.enableRelay !== false && !listenAddrs.includes('/p2p-circuit')) { listenAddrs.push('/p2p-circuit'); } console.log('[P2P] Listen addresses config:', listenAddrs); console.log('[P2P] Peer discovery count:', peerDiscovery.length); const config = { privateKey: this.config.privateKey, addresses: { listen: listenAddrs, announce: this.config.announce || [], }, transports, connectionEncrypters: [noise()], streamMuxers: [yamux()], peerDiscovery, services, connectionManager: { maxConnections: this.config.connectionManager?.maxConnections ?? 50, }, }; if (isBrowser()) { config.connectionGater = { denyDialMultiaddr: () => false, }; } this.node = await createLibp2p(config); this._setupEventHandlers(); this._registerProtocolStreamHandlers(); await this.node.start(); } async stop() { if (this.cleanupIntervalId) { clearInterval(this.cleanupIntervalId); this.cleanupIntervalId = undefined; } if (this.node) { await this.node.stop(); this.node = undefined; } this.protocolHandlers.clear(); this.seenMessages.clear(); this.dhtValues.clear(); this.peerInfo.clear(); this.topicHandlers.clear(); this.coreSecurityManager.removeAllListeners(); this.removeAllListeners(); } get peerId() { if (!this.node) { throw new Error('Node not started'); } return this.node.peerId.toString(); } get libp2pNode() { if (!this.node) { throw new Error('Node not started'); } return this.node; } registerProtocol(handler) { if (this.protocolHandlers.has(handler.protocolName)) { throw new Error(`Protocol already registered: ${handler.protocolName}`); } this.protocolHandlers.set(handler.protocolName, handler); if (this.node && handler.handleStream) { const streamHandler = async (stream, connection) => { try { await handler.handleStream(stream, connection); } catch (error) { console.error(`Error in stream handler for ${handler.protocolName}:`, error); } }; this.node.handle(handler.protocolId, streamHandler); } } unregisterProtocol(protocolName) { const handler = this.protocolHandlers.get(protocolName); if (handler && this.node) { this.node.unhandle(handler.protocolId); } this.protocolHandlers.delete(protocolName); } async connectToPeer(peerAddr) { if (!this.node) { throw new Error('Node not started'); } const ma = typeof peerAddr === 'string' ? multiaddr(peerAddr) : peerAddr; await this.node.dial(ma); } async disconnectFromPeer(peerId) { if (!this.node) { throw new Error('Node not started'); } const parsedPeerId = peerIdFromString(peerId); const connections = this.node.getConnections(parsedPeerId); await Promise.all(connections.map(conn => conn.close({ signal: AbortSignal.timeout(2000), }))); } async sendTo(peerId, message, protocolId) { if (!this.node) { throw new Error('Node not started'); } const protocol = protocolId || '/lotus/message/1.0.0'; const parsedPeerId = peerIdFromString(peerId); const stream = await this.node.dialProtocol(parsedPeerId, protocol); try { const serialized = this.protocol.serialize(message); stream.send(serialized); } finally { await stream.close(); } } async broadcast(message, options) { if (!this.node) { throw new Error('Node not started'); } const peers = this.node.getPeers(); let targetPeers = peers; if (options?.exclude) { targetPeers = targetPeers.filter(p => !options.exclude.includes(p.toString())); } if (options?.includedOnly) { targetPeers = targetPeers.filter(p => options.includedOnly.includes(p.toString())); } const promises = targetPeers .filter(peer => { const connections = this.libp2pNode.getConnections(peer); const hasDirectConnection = connections.some(conn => { const addr = conn.remoteAddr?.toString() || ''; return !addr.includes('/p2p-circuit'); }); return hasDirectConnection; }) .map(peer => this.sendTo(peer.toString(), message, options?.protocol).catch(error => { console.error(`Failed to send to peer ${peer.toString()}:`, error); })); await Promise.all(promises); const peerInfo = { peerId: this.peerId, lastSeen: Date.now(), }; const handler = this.protocolHandlers.get(message.protocol || ''); if (handler) { await handler.handleMessage(message, peerInfo).catch(error => { console.error('[P2P] Error processing self-broadcast:', error); }); } } async announceResource(resourceType, resourceId, data, options) { if (!this.node) { throw new Error('Node not started'); } const peerId = this.node.peerId.toString(); const announcement = { resourceId, resourceType, creatorPeerId: peerId, data, createdAt: Date.now(), expiresAt: options?.expiresAt, }; const key = this._makeResourceKey(resourceType, resourceId); this.dhtValues.set(key, announcement); if (this.node.services.kadDHT && this.config.enableDHTServer) { const dhtStats = this.getDHTStats(); if (dhtStats.isReady) { const dht = this.node.services.kadDHT; const keyBytes = Buffer.from(key, 'utf8'); const valueBytes = Buffer.from(JSON.stringify(announcement), 'utf8'); await this._putDHT(keyBytes, valueBytes, 5000); } } this.emit('resource:announced', announcement); } getLocalResources(resourceType, filters) { const results = []; for (const [key, announcement] of this.dhtValues.entries()) { if (announcement.resourceType === resourceType) { if (this._matchesFilters(announcement, filters)) { if (!announcement.expiresAt || announcement.expiresAt > Date.now()) { results.push(announcement); } } } } return results; } getResource(resourceType, resourceId) { const key = this._makeResourceKey(resourceType, resourceId); const cached = this.dhtValues.get(key); if (cached) { if (!cached.expiresAt || cached.expiresAt > Date.now()) { return cached; } } return null; } async discoverResource(resourceType, resourceId, timeoutMs = 5000) { const key = this._makeResourceKey(resourceType, resourceId); const cached = this.dhtValues.get(key); if (cached && (!cached.expiresAt || cached.expiresAt > Date.now())) { return cached; } if (this.node?.services.kadDHT) { const dhtStats = this.getDHTStats(); if (dhtStats.isReady) { return this._queryDHT(key, timeoutMs); } } return null; } async _queryDHT(key, timeoutMs) { if (!this.node?.services.kadDHT) { return null; } const dht = this.node.services.kadDHT; const keyBytes = Buffer.from(key, 'utf8'); const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, timeoutMs); try { let eventCount = 0; const maxEvents = 20; for await (const event of dht.get(keyBytes, { signal: controller.signal, })) { eventCount++; if (event.name === 'VALUE') { const valueStr = Buffer.from(event.value).toString('utf8'); const announcement = JSON.parse(valueStr); if (announcement.expiresAt && announcement.expiresAt < Date.now()) { const expiredAgo = Math.round((Date.now() - announcement.expiresAt) / 1000); console.warn(`[P2P] DHT returned expired entry (expired ${expiredAgo}s ago): ${key}`); continue; } this.dhtValues.set(key, announcement); clearTimeout(timeout); controller.abort(); return announcement; } if (eventCount >= maxEvents) { controller.abort(); break; } } } catch (error) { if (error.name !== 'AbortError') { console.error('Error querying DHT:', error); } } finally { clearTimeout(timeout); } return null; } async _putDHT(keyBytes, valueBytes, timeoutMs) { if (!this.node?.services.kadDHT) { return; } const dht = this.node.services.kadDHT; const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, timeoutMs); try { let eventCount = 0; const maxEvents = 20; for await (const event of dht.put(keyBytes, valueBytes, { signal: controller.signal, })) { eventCount++; if (eventCount >= maxEvents) { controller.abort(); break; } } } catch (error) { if (error.name !== 'AbortError') { console.error('Error storing in DHT:', error); } } finally { clearTimeout(timeout); } } getConnectedPeers() { if (!this.node) { return []; } const peers = this.node.getPeers(); return peers.map(peerId => { const cached = this.peerInfo.get(peerId.toString()); if (cached) { return cached; } const connections = this.libp2pNode.getConnections(peerId); const multiaddrs = connections.flatMap(conn => conn.remoteAddr ? [conn.remoteAddr.toString()] : []); return { peerId: peerId.toString(), multiaddrs, lastSeen: Date.now(), }; }); } getPeer(peerId) { return this.peerInfo.get(peerId); } isConnected(peerId) { if (!this.node) { return false; } const parsedPeerId = peerIdFromString(peerId); const connections = this.node.getConnections(parsedPeerId); return connections.length > 0; } getStats() { if (!this.node) { return { peerId: 'not-started', peers: { total: 0, connected: 0 }, dht: { enabled: false, mode: 'disabled', routingTableSize: 0, localRecords: 0, }, multiaddrs: [], }; } const peers = this.node.getPeers(); const multiaddrs = this.node.getMultiaddrs(); const dhtStats = this.getDHTStats(); return { peerId: this.node.peerId.toString(), peers: { total: peers.length, connected: peers.length, }, dht: { enabled: dhtStats.enabled, mode: dhtStats.mode, routingTableSize: dhtStats.routingTableSize, localRecords: this.dhtValues.size, }, multiaddrs: multiaddrs.map(ma => ma.toString()), }; } async getReachableAddresses() { if (!this.node) { return []; } const announcedAddrs = this.node.getMultiaddrs(); const relayCircuitAddrs = this._constructRelayCircuitAddresses(); if (relayCircuitAddrs.length > 0) { console.log(`[P2P] Using ${relayCircuitAddrs.length} relay circuit addresses for NAT traversal`); return relayCircuitAddrs; } try { const peer = await this.node.peerStore.get(this.node.peerId); if (peer?.addresses) { const observableAddrs = peer.addresses.map(addr => addr.toString()); const publicAddrs = observableAddrs.filter((addr) => { if (addr.includes('/ip4/127.') || addr.includes('/ip6/::1/')) { return false; } if (addr.includes('/ip4/0.0.0.0/')) { return false; } const ipv4Match = addr.match(/\/ip4\/(\d+\.\d+\.\d+\.\d+)\//); if (ipv4Match) { const ip = ipv4Match[1]; const octets = ip.split('.').map(Number); if (octets[0] === 10) return false; if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return false; if (octets[0] === 192 && octets[1] === 168) return false; if (octets[0] === 169 && octets[1] === 254) return false; if (octets[0] === 127) return false; } return true; }); if (publicAddrs.length > 0) { console.log(`[P2P] Using ${publicAddrs.length} public addresses`); return publicAddrs; } } } catch (error) { console.debug('[P2P] Could not get observable addresses, falling back to relay circuits'); } console.log(`[P2P] No public addresses available, using relay circuits`); return this._constructRelayCircuitAddresses(); } _constructRelayCircuitAddresses() { if (!this.node) { return []; } const circuitAddrs = []; try { if (this.config.bootstrapPeers) { const connections = this.node.getConnections(); for (const bootstrapAddr of this.config.bootstrapPeers) { const parts = bootstrapAddr.split('/p2p/'); if (parts.length === 2) { const relayPeerId = parts[1]; const isConnected = connections.some(conn => conn.remotePeer.toString() === relayPeerId); if (isConnected) { const circuitAddr = bootstrapAddr + '/p2p-circuit/p2p/' + this.node.peerId.toString(); circuitAddrs.push(circuitAddr); console.log(`[P2P] Bootstrap relay circuit: ${bootstrapAddr} → ${circuitAddr}`); } } } } } catch (error) { console.debug('[P2P] Error constructing relay circuit addresses:', error); } return circuitAddrs; } async hasRelayAddresses() { const reachableAddrs = await this.getReachableAddresses(); return reachableAddrs.some((addr) => addr.includes('/p2p-circuit/p2p/')); } async getRelayAddresses() { const reachableAddrs = await this.getReachableAddresses(); return reachableAddrs.filter((addr) => addr.includes('/p2p-circuit/p2p/')); } getConnectionStats() { if (!this.node) { return { totalConnections: 0, connectedPeers: [], }; } const connections = this.node.getConnections(); const connectedPeers = connections.map(conn => conn.remotePeer.toString()); return { totalConnections: connections.length, connectedPeers, }; } async _checkAndNotifyRelayAddresses() { try { const currentAddrs = await this.getReachableAddresses(); const hasNewRelayAddrs = currentAddrs.some((addr) => addr.includes('/p2p-circuit/p2p/') && !this.lastAdvertisedMultiaddrs.includes(addr)); if (hasNewRelayAddrs) { console.log('[P2P] New relay addresses detected (periodic check)'); this.lastAdvertisedMultiaddrs = [...currentAddrs]; this.emit(RelayEvent.ADDRESSES_AVAILABLE, { peerId: this.peerId, reachableAddresses: currentAddrs, relayAddresses: currentAddrs.filter((addr) => addr.includes('/p2p-circuit/p2p/')), timestamp: Date.now(), }); const relayData = { peerId: this.peerId, reachableAddresses: currentAddrs, relayAddresses: currentAddrs.filter((addr) => addr.includes('/p2p-circuit/p2p/')), timestamp: Date.now(), }; for (const handler of this.protocolHandlers.values()) { if (handler.onRelayAddressesChanged) { handler.onRelayAddressesChanged(relayData).catch(error => { console.error(`Error in onRelayAddressesChanged for ${handler.protocolName}:`, error); }); } } } } catch (error) { console.debug('[P2P] Relay address check error:', error); } } getDHTStats() { if (!this.node?.services.kadDHT) { return { enabled: false, mode: 'disabled', routingTableSize: 0, isReady: false, }; } const dht = this.node.services.kadDHT; const routingTableSize = dht.routingTable?.size ?? 0; let mode = 'disabled'; if (dht.getMode) { mode = dht.getMode(); } else { mode = this.config.enableDHTServer ? 'server' : 'client'; } const isReady = routingTableSize > 0; return { enabled: true, mode, routingTableSize, isReady, }; } cleanup() { const now = Date.now(); for (const [key, announcement] of this.dhtValues.entries()) { if (announcement.expiresAt && announcement.expiresAt < now) { this.dhtValues.delete(key); this.coreSecurityManager.resourceTracker.removeResource(announcement.creatorPeerId, announcement.resourceType, announcement.resourceId); } } this.coreSecurityManager.cleanup(); } async shutdown() { if (this.cleanupIntervalId) { clearInterval(this.cleanupIntervalId); this.cleanupIntervalId = undefined; } if (this.node) { await this.node.stop(); this.node = undefined; } this.protocolHandlers.clear(); this.seenMessages.clear(); this.dhtValues.clear(); this.peerInfo.clear(); this.topicHandlers.clear(); this.coreSecurityManager.removeAllListeners(); this.removeAllListeners(); } _setupEventHandlers() { if (!this.node) { return; } this.node.addEventListener('self:peer:update', event => { console.log('[P2P] Self peer updated - checking for relay address changes'); this._checkAndNotifyRelayAddresses().catch(error => { console.debug('[P2P] Error checking relay addresses:', error); }); }); this.node.addEventListener('peer:connect', event => { const peerId = event.detail.toString(); console.log('[P2P] Peer connected:', peerId); const existing = this.peerInfo.get(peerId); const connections = this.libp2pNode.getConnections(event.detail); const multiaddrs = connections.flatMap(conn => conn.remoteAddr ? [conn.remoteAddr.toString()] : []); const peerInfo = { peerId, multiaddrs: multiaddrs.length > 0 ? multiaddrs : existing?.multiaddrs, publicKey: existing?.publicKey, metadata: existing?.metadata, lastSeen: Date.now(), }; this.peerInfo.set(peerId, peerInfo); this.emit(ConnectionEvent.CONNECTED, peerInfo); for (const handler of this.protocolHandlers.values()) { handler.onPeerConnected?.(peerId).catch(error => { console.error(`Error in onPeerConnected for ${handler.protocolName}:`, error); }); } }); this.node.addEventListener('peer:disconnect', event => { const peerId = event.detail.toString(); const peerInfo = this.peerInfo.get(peerId); if (peerInfo) { this.emit(ConnectionEvent.DISCONNECTED, peerInfo); } for (const handler of this.protocolHandlers.values()) { handler.onPeerDisconnected?.(peerId).catch((error) => { console.error(`Error in onPeerDisconnected for ${handler.protocolName}:`, error); }); } }); this.node.addEventListener('peer:discovery', event => { const detail = event.detail; const peerId = detail.id.toString(); const multiaddrs = detail.multiaddrs.map(ma => ma.toString()); console.log('[P2P] Peer discovered:', peerId, multiaddrs); const peerInfo = { peerId, multiaddrs, lastSeen: Date.now(), }; this.peerInfo.set(peerId, peerInfo); this.emit(ConnectionEvent.DISCOVERED, peerInfo); for (const handler of this.protocolHandlers.values()) { handler.onPeerDiscovered?.(peerInfo).catch(error => { console.error(`Error in onPeerDiscovered for ${handler.protocolName}:`, error); }); } }); this.node.addEventListener('peer:update', event => { const detail = event.detail; const peer = detail.peer; const peerId = peer.id.toString(); const existing = this.peerInfo.get(peerId); const connections = this.libp2pNode.getConnections(peer.id); const multiaddrs = connections.flatMap(conn => conn.remoteAddr ? [conn.remoteAddr.toString()] : []); const peerInfo = { peerId, multiaddrs: multiaddrs.length > 0 ? multiaddrs : existing?.multiaddrs, publicKey: existing?.publicKey, metadata: existing?.metadata, lastSeen: Date.now(), }; this.peerInfo.set(peerId, peerInfo); this.emit(ConnectionEvent.UPDATED, peerInfo); for (const handler of this.protocolHandlers.values()) { handler.onPeerUpdated?.(peerInfo).catch(error => { console.error(`Error in onPeerUpdated for ${handler.protocolName}:`, error); }); } }); const messageHandler = async (stream, connection) => { try { await this._handleIncomingStream(stream, connection); } catch (error) { console.error('Error handling message stream:', error); } }; this.node.handle('/lotus/message/1.0.0', messageHandler); } _registerProtocolStreamHandlers() { if (!this.node) { throw new Error('Cannot register protocol stream handlers: node not created'); } for (const handler of this.protocolHandlers.values()) { if (handler.handleStream) { const streamHandler = async (stream, connection) => { try { await handler.handleStream(stream, connection); } catch (error) { console.error(`Error in stream handler for ${handler.protocolName}:`, error); } }; this.node.handle(handler.protocolId, streamHandler); } } } async _handleIncomingStream(stream, connection) { try { const data = []; let totalSize = 0; const MAX_MESSAGE_SIZE = 100_000; for await (const chunk of stream) { if (chunk instanceof Uint8Array) { totalSize += chunk.length; if (totalSize > MAX_MESSAGE_SIZE) { console.warn(`[P2P] Oversized message from ${connection.remotePeer.toString()}: ${totalSize} bytes (max: ${MAX_MESSAGE_SIZE})`); this.coreSecurityManager.recordMessage(false, true); this.coreSecurityManager.peerBanManager.warnPeer(connection.remotePeer.toString(), 'oversized-message'); stream.abort(new Error('Message too large')); return; } data.push(chunk.subarray()); } else { totalSize += chunk.length; if (totalSize > MAX_MESSAGE_SIZE) { console.warn(`[P2P] Oversized message from ${connection.remotePeer.toString()}: ${totalSize} bytes (max: ${MAX_MESSAGE_SIZE})`); this.coreSecurityManager.recordMessage(false, true); this.coreSecurityManager.peerBanManager.warnPeer(connection.remotePeer.toString(), 'oversized-message'); stream.abort(new Error('Message too large')); return; } data.push(chunk.subarray()); } } if (data.length === 0) { return; } const combined = Buffer.concat(data.map(d => Buffer.from(d))); if (combined.length === 0) { return; } const message = this.protocol.deserialize(combined); if (!this.protocol.validateMessage(message)) { console.warn('Invalid message received'); this.coreSecurityManager.recordMessage(false); this.coreSecurityManager.peerBanManager.warnPeer(connection.remotePeer.toString(), 'invalid-message-format'); return; } this.coreSecurityManager.recordMessage(true); const messageHash = this.protocol.computeMessageHash(message); if (this.seenMessages.has(messageHash)) { return; } this.seenMessages.add(messageHash); if (this.seenMessages.size > 10000) { const toRemove = Array.from(this.seenMessages).slice(0, 1000); toRemove.forEach(hash => this.seenMessages.delete(hash)); } const from = { peerId: connection.remotePeer.toString(), lastSeen: Date.now(), }; this.emit(ConnectionEvent.MESSAGE, message, from); if (message.protocol) { const handler = this.protocolHandlers.get(message.protocol); if (handler) { await handler.handleMessage(message, from); } } } catch (error) { console.error('Error processing incoming message:', error); } } _matchesFilters(announcement, filters) { if (!filters) { return true; } for (const [key, value] of Object.entries(filters)) { if (announcement.data && typeof announcement.data === 'object') { const data = announcement.data; if (data[key] !== value) { return false; } } } return true; } _makeResourceKey(resourceType, resourceId) { return `resource:${resourceType}:${resourceId}`; } async subscribeToTopic(topic, handler) { if (!this.node) { throw new Error('Node not started'); } const pubsub = this.node.services.pubsub; if (!pubsub) { throw new Error('GossipSub not enabled in config'); } if (this.topicHandlers.has(topic)) { const existingHandler = this.topicHandlers.get(topic); pubsub.removeEventListener('message', existingHandler); this.topicHandlers.delete(topic); } pubsub.subscribe(topic); const messageHandler = (evt) => { if (evt.detail.topic === topic) { handler(evt.detail.data); } }; this.topicHandlers.set(topic, messageHandler); pubsub.addEventListener('message', messageHandler); console.log(`[P2P] Subscribed to topic: ${topic}`); } async unsubscribeFromTopic(topic) { if (!this.node) { return; } const pubsub = this.node.services.pubsub; if (!pubsub) { return; } const handler = this.topicHandlers.get(topic); if (handler) { pubsub.removeEventListener('message', handler); this.topicHandlers.delete(topic); } pubsub.unsubscribe(topic); console.log(`[P2P] Unsubscribed from topic: ${topic}`); } async publishToTopic(topic, message) { if (!this.node) { throw new Error('Node not started'); } const pubsub = this.node.services.pubsub; if (!pubsub) { throw new Error('GossipSub not enabled in config'); } const messageStr = JSON.stringify(message); const messageBytes = new Uint8Array(Buffer.from(messageStr, 'utf8')); await pubsub.publish(topic, messageBytes); console.log(`[P2P] Published to topic: ${topic}`); } getTopicPeers(topic) { if (!this.node) { return []; } const pubsub = this.node.services.pubsub; if (!pubsub) { return []; } const peers = pubsub.getSubscribers(topic); return Array.from(peers).map(p => p.toString()); } }