From 303b3fa9f7d2dd856615145490d7bc59eed9423f Mon Sep 17 00:00:00 2001 From: ale Date: Tue, 17 Jun 2025 00:15:09 +0200 Subject: [PATCH] feat: implement automatic ring positioning - Remove manual --position parameter from CLI - Add automatic optimal position calculation based on gap analysis - Implement dynamic ring topology management - Add position tracking and synchronization across nodes - Add topology command for visual ring structure inspection - Clean up unused variables and dead code - Simplify node setup with automatic positioning --- README.md | 30 +++- config/ice-servers-default.json | 10 -- config/ice-servers-public-turn.json | 21 --- config/ice-servers-with-turn.json | 15 -- node.js | 32 +++- oracle.js | 32 +++- src/ring-node.js | 246 ++++++++++++++++++++++++---- 7 files changed, 299 insertions(+), 87 deletions(-) delete mode 100644 config/ice-servers-default.json delete mode 100644 config/ice-servers-public-turn.json delete mode 100644 config/ice-servers-with-turn.json diff --git a/README.md b/README.md index 5de9556..724d55c 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,13 @@ npm run start:node -- --port 8083 --bootstrap localhost:8080 --port # Port to listen on (default: random) --id # Node ID (default: auto-generated UUID) --bootstrap # Bootstrap node address (host:port) ---position # Initial ring position (default: 0) --ice-servers # ICE servers configuration (JSON array) --config # Load configuration from JSON file --help # Show help message ``` +**Note**: Ring positions are now assigned automatically for optimal network topology. The system calculates the best position for each node to ensure even distribution around the ring. + ### WebRTC ICE Servers Configuration The Ring Network uses WebRTC for peer-to-peer connections. You can configure custom ICE servers (STUN/TURN) for better connectivity: @@ -137,6 +138,7 @@ Once a node is running, you can use these commands: - `info` - Show network information - `peers` - List connected peers - `connections` - Show persistent connection status +- `topology` - Show ring topology and node positions - `help` - Show available commands - `quit` - Exit the node @@ -148,6 +150,7 @@ Once a node is running, you can use these commands: - `get ` - Retrieve data from storage - `propose ` - Create a consensus proposal - `vote ` - Vote on a proposal +- `topology` - Show ring topology and node positions ## ๐Ÿ”ฎ Oracle Services @@ -183,6 +186,31 @@ Oracle nodes provide enhanced services to the network: - Usage analytics - Historical data tracking +## ๐ŸŽฏ Automatic Ring Positioning + +The Ring Network now features **automatic node positioning** that optimizes network topology without manual configuration: + +### Key Features +- **Optimal Placement**: New nodes are automatically positioned in the largest gap between existing nodes +- **Even Distribution**: Nodes are distributed evenly around the virtual ring for balanced load +- **Dynamic Rebalancing**: Network topology adjusts automatically when nodes join or leave +- **No Manual Configuration**: Eliminates the need to manually specify ring positions + +### How It Works +1. **Virtual Ring**: The network uses a virtual ring of 1000 positions +2. **Gap Analysis**: When a new node joins, the system finds the largest gap between existing nodes +3. **Optimal Insertion**: The new node is placed in the middle of the largest gap +4. **Topology Update**: All nodes update their neighbor connections based on the new topology +5. **Automatic Rebalancing**: The system can redistribute nodes evenly when needed + +### Benefits +- **Simplified Setup**: No need to calculate or specify positions manually +- **Better Performance**: Optimal positioning improves routing efficiency +- **Self-Healing**: Network automatically adapts to node changes +- **Scalability**: Easy to add new nodes without topology planning + +Use the `topology` command in any node to view the current ring structure and positions. + ## ๐Ÿงช Testing Run the test suite to verify network functionality: diff --git a/config/ice-servers-default.json b/config/ice-servers-default.json deleted file mode 100644 index 4ed756f..0000000 --- a/config/ice-servers-default.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "iceServers": [ - { - "urls": "stun:stun.l.google.com:19302" - }, - { - "urls": "stun:stun1.l.google.com:19302" - } - ] -} diff --git a/config/ice-servers-public-turn.json b/config/ice-servers-public-turn.json deleted file mode 100644 index 4f78689..0000000 --- a/config/ice-servers-public-turn.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "iceServers": [ - { - "urls": [ - "stun:stun.l.google.com:19302", - "stun:stun1.l.google.com:19302", - "stun:stun2.l.google.com:19302" - ] - }, - { - "urls": "turn:openrelay.metered.ca:80", - "username": "openrelayproject", - "credential": "openrelayproject" - }, - { - "urls": "turn:openrelay.metered.ca:443", - "username": "openrelayproject", - "credential": "openrelayproject" - } - ] -} diff --git a/config/ice-servers-with-turn.json b/config/ice-servers-with-turn.json deleted file mode 100644 index 6190772..0000000 --- a/config/ice-servers-with-turn.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "iceServers": [ - { - "urls": "stun:stun.l.google.com:19302" - }, - { - "urls": "stun:stun1.l.google.com:19302" - }, - { - "urls": "turn:turnserver.example.com:3478", - "username": "your_username", - "credential": "your_password" - } - ] -} diff --git a/node.js b/node.js index 2dd5386..9f872da 100644 --- a/node.js +++ b/node.js @@ -19,9 +19,6 @@ for (let i = 0; i < args.length; i++) { case '--bootstrap': options.bootstrap = args[++i]; break; - case '--position': - options.ringPosition = parseInt(args[++i]); - break; case '--ice-servers': try { options.iceServers = JSON.parse(args[++i]); @@ -52,7 +49,6 @@ Options: --port Port to listen on (default: random) --id Node ID (default: auto-generated) --bootstrap Bootstrap node address (host:port) - --position Initial ring position (default: 0) --ice-servers ICE servers configuration (JSON array) --config Load configuration from JSON file --help Show this help message @@ -62,6 +58,8 @@ Examples: node node.js --port 8081 --bootstrap localhost:8080 node node.js --id mynode --port 8082 --bootstrap localhost:8080 node node.js --config config/ice-servers-with-turn.json --port 8080 + +Note: Ring positions are now assigned automatically for optimal network topology. ICE Servers Example: --ice-servers '[{"urls":"stun:stun.l.google.com:19302"},{"urls":"turn:turn.example.com:3478","username":"user","credential":"pass"}]' @@ -175,6 +173,31 @@ function handleCommand(command) { } break; + case 'topology': + const topology = node.getRingTopology(); + console.log(chalk.blue('\n๐Ÿ”„ Ring Topology:')); + console.log(` Ring Size: ${topology.ringSize}`); + console.log(` Total Nodes: ${topology.totalNodes}`); + console.log(` My Position: ${node.ringPosition}`); + + if (topology.nodes.length > 0) { + console.log(chalk.yellow('\n๐Ÿ“ Node Positions:')); + topology.nodes.forEach(nodeInfo => { + const indicators = []; + if (nodeInfo.isThisNode) indicators.push(chalk.green('ME')); + if (nodeInfo.isOracle) indicators.push(chalk.yellow('๐Ÿ”ฎ')); + if (nodeInfo.isConnected) indicators.push(chalk.green('โœ“')); + + const indicatorStr = indicators.length > 0 ? ` [${indicators.join(' ')}]` : ''; + console.log(` ${nodeInfo.position.toString().padStart(4)}: ${nodeInfo.nodeId}...${indicatorStr}`); + }); + + console.log(chalk.cyan('\n๐Ÿ”„ Ring Connections:')); + console.log(` Inner Ring: ${topology.innerRing.left}... โ† ME โ†’ ${topology.innerRing.right}...`); + console.log(` Outer Ring: ${topology.outerRing.left}... โ† ME โ†’ ${topology.outerRing.right}...`); + } + break; + case 'help': console.log(chalk.blue(` ๐Ÿ“š Available Commands: @@ -182,6 +205,7 @@ function handleCommand(command) { info - Show network information peers - List connected peers connections - Show persistent connection status + topology - Show ring topology and positions help - Show this help quit - Exit the node `)); diff --git a/oracle.js b/oracle.js index 5ebda92..d6edb52 100644 --- a/oracle.js +++ b/oracle.js @@ -19,9 +19,6 @@ for (let i = 0; i < args.length; i++) { case '--bootstrap': options.bootstrap = args[++i]; break; - case '--position': - options.ringPosition = parseInt(args[++i]); - break; case '--ice-servers': try { options.iceServers = JSON.parse(args[++i]); @@ -52,7 +49,6 @@ Options: --port Port to listen on (default: random) --id Node ID (default: auto-generated) --bootstrap Bootstrap node address (host:port) - --position Initial ring position (default: 0) --ice-servers ICE servers configuration (JSON array) --config Load configuration from JSON file --help Show this help message @@ -63,6 +59,8 @@ Examples: node oracle.js --id oracle1 --port 8082 --bootstrap localhost:8080 node oracle.js --config config/ice-servers-with-turn.json --port 8080 +Note: Ring positions are now assigned automatically for optimal network topology. + ICE Servers Example: --ice-servers '[{"urls":"stun:stun.l.google.com:19302"},{"urls":"turn:turn.example.com:3478","username":"user","credential":"pass"}]' @@ -240,6 +238,31 @@ async function handleCommand(command) { } break; + case 'topology': + const topology = oracle.getRingTopology(); + console.log(chalk.blue('\n๐Ÿ”„ Ring Topology:')); + console.log(` Ring Size: ${topology.ringSize}`); + console.log(` Total Nodes: ${topology.totalNodes}`); + console.log(` My Position: ${oracle.ringPosition}`); + + if (topology.nodes.length > 0) { + console.log(chalk.yellow('\n๐Ÿ“ Node Positions:')); + topology.nodes.forEach(nodeInfo => { + const indicators = []; + if (nodeInfo.isThisNode) indicators.push(chalk.green('ME')); + if (nodeInfo.isOracle) indicators.push(chalk.yellow('๐Ÿ”ฎ')); + if (nodeInfo.isConnected) indicators.push(chalk.green('โœ“')); + + const indicatorStr = indicators.length > 0 ? ` [${indicators.join(' ')}]` : ''; + console.log(` ${nodeInfo.position.toString().padStart(4)}: ${nodeInfo.nodeId}...${indicatorStr}`); + }); + + console.log(chalk.cyan('\n๐Ÿ”„ Ring Connections:')); + console.log(` Inner Ring: ${topology.innerRing.left}... โ† ME โ†’ ${topology.innerRing.right}...`); + console.log(` Outer Ring: ${topology.outerRing.left}... โ† ME โ†’ ${topology.outerRing.right}...`); + } + break; + case 'help': console.log(chalk.yellow(` ๐Ÿ”ฎ Oracle Commands: @@ -253,6 +276,7 @@ async function handleCommand(command) { get - Retrieve data from storage propose - Create a consensus proposal vote - Vote on a proposal + topology - Show ring topology and positions help - Show this help quit - Exit the oracle `)); diff --git a/src/ring-node.js b/src/ring-node.js index bfd90cf..39dd274 100644 --- a/src/ring-node.js +++ b/src/ring-node.js @@ -9,7 +9,7 @@ export class RingNode extends EventEmitter { super(); this.id = options.id || uuidv4(); this.port = options.port || this.getRandomPort(); - this.ringPosition = options.ringPosition || 0; + this.ringPosition = null; // Will be assigned automatically this.isOracle = options.isOracle || false; // Two rings: inner and outer @@ -32,6 +32,11 @@ export class RingNode extends EventEmitter { this.messageHistory = new Set(); this.oracleNodes = new Set(); this.activeSignalingConnections = new Map(); // Store active WebSocket connections for signaling + this.nodePositions = new Map(); // Track all node positions in the ring + this.ringSize = 1000; // Virtual ring size for position calculation + + // Assign initial position for this node + this.assignPosition(this.id); this.setupWebRTCHandlers(); this.setupDiscoveryServer(); @@ -395,6 +400,21 @@ export class RingNode extends EventEmitter { if (message.success) { bootstrapNodeId = message.bootstrapNodeId; + // Update position if provided by bootstrap node + if (message.assignedPosition !== undefined) { + this.ringPosition = message.assignedPosition; + this.nodePositions.set(this.id, message.assignedPosition); + console.log(chalk.green(`๐Ÿ“ Assigned position ${message.assignedPosition} in the ring`)); + } + + // Update network topology if provided + if (message.networkTopology) { + Object.entries(message.networkTopology).forEach(([nodeId, position]) => { + this.nodePositions.set(nodeId, position); + }); + this.updateRingTopology(); + } + clearTimeout(timeoutId); ws.close(); resolve(true); @@ -450,16 +470,18 @@ export class RingNode extends EventEmitter { this.oracleNodes.add(nodeId); } - // Find optimal position in rings for the new node - await this.integrateNewNode(nodeId); + // Find optimal position for the new node + const assignedPosition = await this.integrateNewNode(nodeId); - // Send success response with bootstrap node info + // Send success response with position and topology info ws.send(JSON.stringify({ type: 'join-response', success: true, message: 'Successfully integrated into ring network', bootstrapNodeId: this.id, - isOracle: this.isOracle + isOracle: this.isOracle, + assignedPosition: assignedPosition, + networkTopology: Object.fromEntries(this.nodePositions) })); // After successful join, try to establish WebRTC connection @@ -490,41 +512,152 @@ export class RingNode extends EventEmitter { message: 'Failed to integrate into ring network' })); } - } - - async integrateNewNode(nodeId) { - // Simple integration: connect as right neighbor in both rings - // In a production system, this would be more sophisticated + } async integrateNewNode(nodeId) { + // Automatically assign optimal position for the new node + const newPosition = this.calculateOptimalPosition(); + this.nodePositions.set(nodeId, newPosition); - const oldInnerRight = this.rings.inner.right; - const oldOuterRight = this.rings.outer.right; - - // Update ring connections - this.rings.inner.right = nodeId; - this.rings.outer.right = nodeId; + // Update ring topology with the new node + this.updateRingTopology(); // Notify the network of topology change this.broadcastNetworkUpdate(); + + console.log(chalk.green(`๐Ÿ”„ Integrated node ${nodeId} at position ${newPosition}`)); + + return newPosition; } + // Automatic position management methods + calculateOptimalPosition() { + // If this is the first node in the ring + if (this.nodePositions.size === 0) { + return 0; + } + + // If there's only one node, place the new one opposite to it + if (this.nodePositions.size === 1) { + return Math.floor(this.ringSize / 2); + } + + // Get all current positions and sort them + const positions = Array.from(this.nodePositions.values()).sort((a, b) => a - b); + + // Find the largest gap between consecutive positions + let largestGap = 0; + let optimalPosition = 0; + + for (let i = 0; i < positions.length; i++) { + const currentPos = positions[i]; + const nextPos = positions[(i + 1) % positions.length]; + + // Calculate gap (considering ring wraparound) + let gap; + if (nextPos > currentPos) { + gap = nextPos - currentPos; + } else { + // Wraparound case + gap = (this.ringSize - currentPos) + nextPos; + } + + if (gap > largestGap) { + largestGap = gap; + // Place new node in the middle of the largest gap + optimalPosition = (currentPos + gap / 2) % this.ringSize; + } + } + + return Math.floor(optimalPosition); + } + + assignPosition(nodeId, position = null) { + if (position === null) { + position = this.calculateOptimalPosition(); + } + + this.nodePositions.set(nodeId, position); + + if (nodeId === this.id) { + this.ringPosition = position; + } + + // Update ring topology based on new positions + this.updateRingTopology(); + + return position; + } + + updateRingTopology() { + // Sort all nodes by their positions + const sortedNodes = Array.from(this.nodePositions.entries()) + .sort((a, b) => a[1] - b[1]); // Sort by position + + // Clear current ring connections + this.rings.inner.left = null; + this.rings.inner.right = null; + this.rings.outer.left = null; + this.rings.outer.right = null; + + if (sortedNodes.length <= 1) { + return; // Not enough nodes for a ring + } + + // Find this node's index in the sorted list + const thisNodeIndex = sortedNodes.findIndex(([nodeId]) => nodeId === this.id); + + if (thisNodeIndex === -1) { + return; // This node not found in positions + } + + const nodeCount = sortedNodes.length; + + // Calculate neighbors in inner ring (clockwise) + const innerRightIndex = (thisNodeIndex + 1) % nodeCount; + const innerLeftIndex = (thisNodeIndex - 1 + nodeCount) % nodeCount; + + this.rings.inner.right = sortedNodes[innerRightIndex][0]; + this.rings.inner.left = sortedNodes[innerLeftIndex][0]; + + // Calculate neighbors in outer ring (counter-clockwise) + this.rings.outer.right = sortedNodes[innerLeftIndex][0]; + this.rings.outer.left = sortedNodes[innerRightIndex][0]; + } + + removeNodePosition(nodeId) { + this.nodePositions.delete(nodeId); + this.updateRingTopology(); + } + + rebalanceRing() { + // Redistribute all nodes evenly around the ring + const nodeIds = Array.from(this.nodePositions.keys()); + const nodeCount = nodeIds.length; + + if (nodeCount === 0) return; + + const step = this.ringSize / nodeCount; + + nodeIds.forEach((nodeId, index) => { + const newPosition = Math.floor(index * step); + this.nodePositions.set(nodeId, newPosition); + + if (nodeId === this.id) { + this.ringPosition = newPosition; + } + }); + + this.updateRingTopology(); + } + handlePeerDisconnection(peerId) { - // Update ring topology when a peer disconnects - if (this.rings.inner.left === peerId) { - this.rings.inner.left = null; - } - if (this.rings.inner.right === peerId) { - this.rings.inner.right = null; - } - if (this.rings.outer.left === peerId) { - this.rings.outer.left = null; - } - if (this.rings.outer.right === peerId) { - this.rings.outer.right = null; - } + // Remove node position and update topology + this.removeNodePosition(peerId); this.knownNodes.delete(peerId); this.oracleNodes.delete(peerId); + console.log(chalk.yellow(`๐Ÿ”„ Node ${peerId} disconnected, ring topology updated`)); + this.broadcastNetworkUpdate(); } @@ -536,7 +669,9 @@ export class RingNode extends EventEmitter { nodeId: this.id, rings: this.rings, knownNodes: Array.from(this.knownNodes.keys()), - oracleNodes: Array.from(this.oracleNodes) + oracleNodes: Array.from(this.oracleNodes), + nodePositions: Object.fromEntries(this.nodePositions), + ringPosition: this.ringPosition } }; @@ -544,7 +679,7 @@ export class RingNode extends EventEmitter { } handleNetworkUpdate(from, payload) { - const { rings, knownNodes, oracleNodes } = payload; + const { rings, knownNodes, oracleNodes, nodePositions, ringPosition } = payload; // Update our knowledge of network topology knownNodes.forEach(nodeId => { @@ -557,6 +692,24 @@ export class RingNode extends EventEmitter { this.oracleNodes.add(nodeId); }); + // Update node positions if provided + if (nodePositions) { + // Merge position information from other nodes + Object.entries(nodePositions).forEach(([nodeId, position]) => { + if (!this.nodePositions.has(nodeId)) { + this.nodePositions.set(nodeId, position); + } + }); + + // Update the sender's position + if (ringPosition !== undefined) { + this.nodePositions.set(from, ringPosition); + } + + // Recalculate our ring topology based on updated positions + this.updateRingTopology(); + } + this.emit('networkUpdate', { from, payload }); } @@ -641,10 +794,13 @@ export class RingNode extends EventEmitter { nodeId: this.id, port: this.port, isOracle: this.isOracle, + ringPosition: this.ringPosition, rings: this.rings, connectedPeers: this.webrtc.getConnectedPeers(), knownNodes: Array.from(this.knownNodes.keys()), - oracleNodes: Array.from(this.oracleNodes) + oracleNodes: Array.from(this.oracleNodes), + nodePositions: Object.fromEntries(this.nodePositions), + ringTopology: this.getRingTopology() }; } @@ -765,4 +921,30 @@ export class RingNode extends EventEmitter { oracleNodes: this.oracleNodes.size }; } + + getRingTopology() { + // Create a visual representation of the ring topology + const sortedNodes = Array.from(this.nodePositions.entries()) + .sort((a, b) => a[1] - b[1]); // Sort by position + + return { + totalNodes: sortedNodes.length, + ringSize: this.ringSize, + nodes: sortedNodes.map(([nodeId, position]) => ({ + nodeId: nodeId.substring(0, 8), // Short version for display + position: position, + isThisNode: nodeId === this.id, + isOracle: this.oracleNodes.has(nodeId), + isConnected: this.webrtc.getConnectedPeers().includes(nodeId) + })), + innerRing: { + left: this.rings.inner.left?.substring(0, 8), + right: this.rings.inner.right?.substring(0, 8) + }, + outerRing: { + left: this.rings.outer.left?.substring(0, 8), + right: this.rings.outer.right?.substring(0, 8) + } + }; + } }