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
Este commit está contenido en:
ale
2025-06-17 00:15:09 +02:00
padre e238b4b307
commit 303b3fa9f7
Se han modificado 7 ficheros con 299 adiciones y 87 borrados

Ver fichero

@@ -77,12 +77,13 @@ npm run start:node -- --port 8083 --bootstrap localhost:8080
--port <port> # Port to listen on (default: random)
--id <id> # Node ID (default: auto-generated UUID)
--bootstrap <addr> # Bootstrap node address (host:port)
--position <pos> # Initial ring position (default: 0)
--ice-servers <json> # ICE servers configuration (JSON array)
--config <file> # 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 <key>` - Retrieve data from storage
- `propose <text>` - Create a consensus proposal
- `vote <id> <yes|no>` - 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:

Ver fichero

@@ -1,10 +0,0 @@
{
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "stun:stun1.l.google.com:19302"
}
]
}

Ver fichero

@@ -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"
}
]
}

Ver fichero

@@ -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"
}
]
}

32
node.js
Ver fichero

@@ -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> Port to listen on (default: random)
--id <id> Node ID (default: auto-generated)
--bootstrap <addr> Bootstrap node address (host:port)
--position <pos> Initial ring position (default: 0)
--ice-servers <json> ICE servers configuration (JSON array)
--config <file> 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
`));

Ver fichero

@@ -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> Port to listen on (default: random)
--id <id> Node ID (default: auto-generated)
--bootstrap <addr> Bootstrap node address (host:port)
--position <pos> Initial ring position (default: 0)
--ice-servers <json> ICE servers configuration (JSON array)
--config <file> 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 <key> - Retrieve data from storage
propose <text> - Create a consensus proposal
vote <id> <yes|no> - Vote on a proposal
topology - Show ring topology and positions
help - Show this help
quit - Exit the oracle
`));

Ver fichero

@@ -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)
}
};
}
}