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
JavaScript
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());
}
}