mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-31 21:49:52 +00:00
feat(edge-net): Add multi-network support for creating and joining edge networks
- Add networks.js with NetworkGenesis, NetworkRegistry, and MultiNetworkManager - Support for public, private (invite-only), and consortium networks - Each network has its own genesis block, QDAG ledger, and peer registry - Network IDs derived from genesis hash for tamper-evident identity - Invite code generation for private networks with base64url encoding New CLI options: --networks List all known networks --discover Discover available networks --create-network Create a new network with custom name/type --network-type Set network type (public/private/consortium) --switch Switch active network for contributions --invite Provide invite code for private networks Security features: - Network isolation with separate storage per network - Cryptographic network identity from genesis hash - Invite codes for access control on private networks - Ed25519 signatures for network announcements Well-known networks: - mainnet: Primary public compute network - testnet: Testing and development network
This commit is contained in:
parent
489134fa70
commit
b33cb670c0
3 changed files with 1007 additions and 11 deletions
|
|
@ -20,6 +20,7 @@ import { webcrypto } from 'crypto';
|
|||
import { performance } from 'perf_hooks';
|
||||
import { homedir } from 'os';
|
||||
import { NetworkManager } from './network.js';
|
||||
import { MultiNetworkManager, NetworkRegistry } from './networks.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
@ -106,7 +107,7 @@ function printHelp() {
|
|||
console.log(`${c('bold', 'USAGE:')}
|
||||
${c('green', 'npx @ruvector/edge-net join')} [options]
|
||||
|
||||
${c('bold', 'OPTIONS:')}
|
||||
${c('bold', 'IDENTITY OPTIONS:')}
|
||||
${c('yellow', '--generate')} Generate new Pi-Key identity without joining
|
||||
${c('yellow', '--key <pubkey>')} Join using existing public key (hex)
|
||||
${c('yellow', '--site <id>')} Set site identifier (default: "edge-contributor")
|
||||
|
|
@ -117,23 +118,38 @@ ${c('bold', 'OPTIONS:')}
|
|||
${c('yellow', '--history')} Show contribution history
|
||||
${c('yellow', '--list')} List all stored identities
|
||||
${c('yellow', '--peers')} List connected peers
|
||||
${c('yellow', '--help')} Show this help message
|
||||
|
||||
${c('bold', 'MULTI-NETWORK OPTIONS:')}
|
||||
${c('yellow', '--networks')} List all known networks
|
||||
${c('yellow', '--discover')} Discover available networks
|
||||
${c('yellow', '--network <id>')} Join/use specific network by ID
|
||||
${c('yellow', '--create-network')} Create a new network with name
|
||||
${c('yellow', '--network-type')} Network type: public, private, consortium
|
||||
${c('yellow', '--network-desc')} Description for new network
|
||||
${c('yellow', '--switch <id>')} Switch active network
|
||||
${c('yellow', '--invite <code>')} Invite code for private networks
|
||||
|
||||
${c('bold', 'EXAMPLES:')}
|
||||
${c('dim', '# Generate new identity and join network')}
|
||||
${c('dim', '# Generate new identity and join default network')}
|
||||
$ npx @ruvector/edge-net join
|
||||
|
||||
${c('dim', '# Generate a new Pi-Key identity only')}
|
||||
$ npx @ruvector/edge-net join --generate
|
||||
${c('dim', '# Discover available networks')}
|
||||
$ npx @ruvector/edge-net join --discover
|
||||
|
||||
${c('dim', '# Export identity for backup')}
|
||||
$ npx @ruvector/edge-net join --export my-identity.key --password mypass
|
||||
${c('dim', '# Create a public research network')}
|
||||
$ npx @ruvector/edge-net join --create-network "ML Research" --network-desc "For ML workloads"
|
||||
|
||||
${c('dim', '# Import and join with existing identity')}
|
||||
$ npx @ruvector/edge-net join --import my-identity.key --password mypass
|
||||
${c('dim', '# Create a private team network')}
|
||||
$ npx @ruvector/edge-net join --create-network "Team Alpha" --network-type private
|
||||
|
||||
${c('dim', '# Join with specific site ID')}
|
||||
$ npx @ruvector/edge-net join --site "my-compute-node"
|
||||
${c('dim', '# Join a specific network')}
|
||||
$ npx @ruvector/edge-net join --network net-abc123
|
||||
|
||||
${c('dim', '# Join a private network with invite code')}
|
||||
$ npx @ruvector/edge-net join --network net-xyz789 --invite <invite-code>
|
||||
|
||||
${c('dim', '# Switch active network')}
|
||||
$ npx @ruvector/edge-net join --switch net-abc123
|
||||
|
||||
${c('bold', 'MULTI-CONTRIBUTOR SETUP:')}
|
||||
Each contributor runs their own node with a unique identity.
|
||||
|
|
@ -146,6 +162,11 @@ ${c('bold', 'MULTI-CONTRIBUTOR SETUP:')}
|
|||
|
||||
${c('dim', 'All nodes automatically discover and connect via P2P gossip.')}
|
||||
|
||||
${c('bold', 'NETWORK TYPES:')}
|
||||
${c('cyan', '🌐 Public')} Anyone can join and discover
|
||||
${c('cyan', '🔒 Private')} Requires invite code to join
|
||||
${c('cyan', '🏢 Consortium')} Requires approval from existing members
|
||||
|
||||
${c('bold', 'IDENTITY INFO:')}
|
||||
${c('cyan', 'Pi-Key:')} 40-byte Ed25519-based identity (π-sized)
|
||||
${c('cyan', 'Public Key:')} 32-byte Ed25519 verification key
|
||||
|
|
@ -413,6 +434,15 @@ function parseArgs(args) {
|
|||
list: false,
|
||||
peers: false,
|
||||
help: false,
|
||||
// Multi-network options
|
||||
network: null, // Network ID to join/use
|
||||
createNetwork: null, // Create new network with name
|
||||
networkType: 'public', // public, private, consortium
|
||||
networkDesc: null, // Network description
|
||||
discoverNetworks: false, // Discover available networks
|
||||
listNetworks: false, // List known networks
|
||||
switchNetwork: null, // Switch active network
|
||||
invite: null, // Invite code for private networks
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
|
|
@ -448,6 +478,32 @@ function parseArgs(args) {
|
|||
case '--peers':
|
||||
opts.peers = true;
|
||||
break;
|
||||
// Multi-network options
|
||||
case '--network':
|
||||
case '-n':
|
||||
opts.network = args[++i];
|
||||
break;
|
||||
case '--create-network':
|
||||
opts.createNetwork = args[++i];
|
||||
break;
|
||||
case '--network-type':
|
||||
opts.networkType = args[++i];
|
||||
break;
|
||||
case '--network-desc':
|
||||
opts.networkDesc = args[++i];
|
||||
break;
|
||||
case '--discover':
|
||||
opts.discoverNetworks = true;
|
||||
break;
|
||||
case '--networks':
|
||||
opts.listNetworks = true;
|
||||
break;
|
||||
case '--switch':
|
||||
opts.switchNetwork = args[++i];
|
||||
break;
|
||||
case '--invite':
|
||||
opts.invite = args[++i];
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
opts.help = true;
|
||||
|
|
@ -764,6 +820,103 @@ async function showPeers() {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle --networks command (list known networks)
|
||||
async function handleListNetworks() {
|
||||
console.log(`${c('bold', 'KNOWN NETWORKS:')}\n`);
|
||||
|
||||
try {
|
||||
const registry = new NetworkRegistry();
|
||||
await registry.load();
|
||||
|
||||
const networks = registry.listNetworks();
|
||||
const active = registry.activeNetwork;
|
||||
|
||||
if (networks.length === 0) {
|
||||
console.log(` ${c('dim', 'No networks registered.')}`);
|
||||
console.log(` ${c('dim', 'Use --discover to find available networks.')}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const network of networks) {
|
||||
const isActive = network.id === active;
|
||||
const status = network.joined ?
|
||||
(isActive ? c('green', '● Active') : c('cyan', '○ Joined')) :
|
||||
c('dim', ' Available');
|
||||
const typeIcon = network.type === 'public' ? '🌐' :
|
||||
network.type === 'private' ? '🔒' : '🏢';
|
||||
|
||||
console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
|
||||
console.log(` ${c('dim', 'ID:')} ${network.id}`);
|
||||
console.log(` ${c('dim', 'Type:')} ${network.type}`);
|
||||
if (network.description) {
|
||||
console.log(` ${c('dim', network.description)}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`${c('dim', 'Use --switch <network-id> to change active network')}\n`);
|
||||
|
||||
} catch (err) {
|
||||
console.log(` ${c('red', '✗')} Error: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --discover command
|
||||
async function handleDiscoverNetworks() {
|
||||
console.log(`${c('cyan', 'Discovering networks...')}\n`);
|
||||
|
||||
try {
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
const networks = await manager.discoverNetworks();
|
||||
|
||||
if (networks.length > 0) {
|
||||
console.log(`\n${c('dim', 'To join a network:')} --network <id> [--invite <code>]`);
|
||||
console.log(`${c('dim', 'To create your own:')} --create-network "Name" [--network-type private]\n`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ${c('red', '✗')} Error: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --create-network command
|
||||
async function handleCreateNetwork(opts) {
|
||||
console.log(`${c('cyan', 'Creating new network...')}\n`);
|
||||
|
||||
try {
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
|
||||
const result = await manager.createNetwork({
|
||||
name: opts.createNetwork,
|
||||
type: opts.networkType,
|
||||
description: opts.networkDesc,
|
||||
});
|
||||
|
||||
console.log(`\n${c('dim', 'To invite others (if private):')} Share the invite codes above`);
|
||||
console.log(`${c('dim', 'To contribute:')} --network ${result.networkId}\n`);
|
||||
|
||||
} catch (err) {
|
||||
console.log(` ${c('red', '✗')} Error: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --switch command
|
||||
async function handleSwitchNetwork(networkId) {
|
||||
console.log(`${c('cyan', `Switching to network ${networkId}...`)}\n`);
|
||||
|
||||
try {
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.switchNetwork(networkId);
|
||||
|
||||
console.log(`\n${c('dim', 'Your contributions will now go to this network.')}\n`);
|
||||
|
||||
} catch (err) {
|
||||
console.log(` ${c('red', '✗')} Error: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show network/QDAG statistics
|
||||
async function showNetworkStats() {
|
||||
console.log(`${c('bold', 'NETWORK STATISTICS:')}\n`);
|
||||
|
|
@ -917,6 +1070,31 @@ async function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle multi-network commands (no WASM needed)
|
||||
if (opts.listNetworks) {
|
||||
printBanner();
|
||||
await handleListNetworks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.discoverNetworks) {
|
||||
printBanner();
|
||||
await handleDiscoverNetworks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.createNetwork) {
|
||||
printBanner();
|
||||
await handleCreateNetwork(opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.switchNetwork) {
|
||||
printBanner();
|
||||
await handleSwitchNetwork(opts.switchNetwork);
|
||||
return;
|
||||
}
|
||||
|
||||
printBanner();
|
||||
await setupPolyfills();
|
||||
|
||||
|
|
|
|||
817
examples/edge-net/pkg/networks.js
Normal file
817
examples/edge-net/pkg/networks.js
Normal file
|
|
@ -0,0 +1,817 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Edge-Net Multi-Network Module
|
||||
*
|
||||
* Enables creation, discovery, and contribution to multiple edge networks.
|
||||
* Each network is cryptographically isolated with its own:
|
||||
* - Genesis block and network ID
|
||||
* - QDAG ledger
|
||||
* - Peer registry
|
||||
* - Access control (public/private/invite-only)
|
||||
*
|
||||
* Security Features:
|
||||
* - Network ID derived from genesis hash (tamper-evident)
|
||||
* - Ed25519 signatures for network announcements
|
||||
* - Optional invite codes for private networks
|
||||
* - Cryptographic proof of network membership
|
||||
*/
|
||||
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
red: '\x1b[31m',
|
||||
};
|
||||
|
||||
const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
|
||||
|
||||
// Network types
|
||||
const NetworkType = {
|
||||
PUBLIC: 'public', // Anyone can join and discover
|
||||
PRIVATE: 'private', // Requires invite code to join
|
||||
CONSORTIUM: 'consortium', // Requires approval from existing members
|
||||
};
|
||||
|
||||
// Well-known public networks (bootstrap)
|
||||
const WELL_KNOWN_NETWORKS = [
|
||||
{
|
||||
id: 'mainnet',
|
||||
name: 'Edge-Net Mainnet',
|
||||
description: 'Primary public compute network',
|
||||
type: NetworkType.PUBLIC,
|
||||
genesisHash: 'edgenet-mainnet-genesis-v1',
|
||||
bootstrapNodes: ['edge-net.ruvector.dev:9000'],
|
||||
created: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'testnet',
|
||||
name: 'Edge-Net Testnet',
|
||||
description: 'Testing and development network',
|
||||
type: NetworkType.PUBLIC,
|
||||
genesisHash: 'edgenet-testnet-genesis-v1',
|
||||
bootstrapNodes: ['testnet.ruvector.dev:9000'],
|
||||
created: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
// Directory structure
|
||||
function getNetworksDir() {
|
||||
const dir = join(homedir(), '.ruvector', 'networks');
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
function getRegistryFile() {
|
||||
return join(getNetworksDir(), 'registry.json');
|
||||
}
|
||||
|
||||
function getNetworkDir(networkId) {
|
||||
const dir = join(getNetworksDir(), networkId);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Network Genesis - defines a network's identity
|
||||
*/
|
||||
export class NetworkGenesis {
|
||||
constructor(options = {}) {
|
||||
this.version = 1;
|
||||
this.name = options.name || 'Custom Network';
|
||||
this.description = options.description || 'A custom edge-net network';
|
||||
this.type = options.type || NetworkType.PUBLIC;
|
||||
this.creator = options.creator || null; // Creator's public key
|
||||
this.creatorSiteId = options.creatorSiteId || 'anonymous';
|
||||
this.created = options.created || new Date().toISOString();
|
||||
this.parameters = {
|
||||
minContributors: options.minContributors || 1,
|
||||
confirmationThreshold: options.confirmationThreshold || 3,
|
||||
creditMultiplier: options.creditMultiplier || 1.0,
|
||||
maxPeers: options.maxPeers || 100,
|
||||
...options.parameters,
|
||||
};
|
||||
this.inviteRequired = this.type !== NetworkType.PUBLIC;
|
||||
this.approvers = options.approvers || []; // For consortium networks
|
||||
this.nonce = options.nonce || randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute network ID from genesis hash
|
||||
*/
|
||||
computeNetworkId() {
|
||||
const data = JSON.stringify({
|
||||
version: this.version,
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
creator: this.creator,
|
||||
created: this.created,
|
||||
parameters: this.parameters,
|
||||
nonce: this.nonce,
|
||||
});
|
||||
|
||||
const hash = createHash('sha256').update(data).digest('hex');
|
||||
return `net-${hash.slice(0, 16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signed genesis block
|
||||
*/
|
||||
createSignedGenesis(signFn) {
|
||||
const genesis = {
|
||||
...this,
|
||||
networkId: this.computeNetworkId(),
|
||||
};
|
||||
|
||||
if (signFn) {
|
||||
const dataToSign = JSON.stringify(genesis);
|
||||
genesis.signature = signFn(dataToSign);
|
||||
}
|
||||
|
||||
return genesis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate invite code for private networks
|
||||
*/
|
||||
generateInviteCode() {
|
||||
if (this.type === NetworkType.PUBLIC) {
|
||||
throw new Error('Public networks do not require invite codes');
|
||||
}
|
||||
|
||||
const networkId = this.computeNetworkId();
|
||||
const secret = randomBytes(16).toString('hex');
|
||||
const code = Buffer.from(`${networkId}:${secret}`).toString('base64url');
|
||||
|
||||
return {
|
||||
code,
|
||||
networkId,
|
||||
validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network Registry - manages known networks
|
||||
*/
|
||||
export class NetworkRegistry {
|
||||
constructor() {
|
||||
this.networks = new Map();
|
||||
this.activeNetwork = null;
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
async load() {
|
||||
try {
|
||||
// Load well-known networks
|
||||
for (const network of WELL_KNOWN_NETWORKS) {
|
||||
this.networks.set(network.id, {
|
||||
...network,
|
||||
isWellKnown: true,
|
||||
joined: false,
|
||||
stats: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Load user's network registry
|
||||
if (existsSync(getRegistryFile())) {
|
||||
const data = JSON.parse(await fs.readFile(getRegistryFile(), 'utf-8'));
|
||||
|
||||
for (const network of data.networks || []) {
|
||||
this.networks.set(network.id, {
|
||||
...network,
|
||||
isWellKnown: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.activeNetwork = data.activeNetwork || null;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
} catch (err) {
|
||||
console.error('Failed to load network registry:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
const data = {
|
||||
version: 1,
|
||||
activeNetwork: this.activeNetwork,
|
||||
networks: Array.from(this.networks.values()).filter(n => !n.isWellKnown),
|
||||
savedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await fs.writeFile(getRegistryFile(), JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new network
|
||||
*/
|
||||
async createNetwork(options, identity) {
|
||||
const genesis = new NetworkGenesis({
|
||||
...options,
|
||||
creator: identity?.publicKey,
|
||||
creatorSiteId: identity?.siteId,
|
||||
});
|
||||
|
||||
const networkId = genesis.computeNetworkId();
|
||||
|
||||
// Create network directory structure
|
||||
const networkDir = getNetworkDir(networkId);
|
||||
await fs.mkdir(join(networkDir, 'peers'), { recursive: true });
|
||||
|
||||
// Save genesis block
|
||||
const genesisData = genesis.createSignedGenesis(
|
||||
identity?.sign ? (data) => identity.sign(data) : null
|
||||
);
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'genesis.json'),
|
||||
JSON.stringify(genesisData, null, 2)
|
||||
);
|
||||
|
||||
// Initialize QDAG for this network
|
||||
const qdag = {
|
||||
networkId,
|
||||
nodes: [{
|
||||
id: 'genesis',
|
||||
type: 'genesis',
|
||||
timestamp: Date.now(),
|
||||
message: `Genesis: ${genesis.name}`,
|
||||
parents: [],
|
||||
weight: 1,
|
||||
confirmations: 0,
|
||||
}],
|
||||
tips: ['genesis'],
|
||||
confirmed: ['genesis'],
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'qdag.json'),
|
||||
JSON.stringify(qdag, null, 2)
|
||||
);
|
||||
|
||||
// Initialize peer list
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'peers.json'),
|
||||
JSON.stringify([], null, 2)
|
||||
);
|
||||
|
||||
// Register network
|
||||
const networkEntry = {
|
||||
id: networkId,
|
||||
name: genesis.name,
|
||||
description: genesis.description,
|
||||
type: genesis.type,
|
||||
creator: genesis.creator,
|
||||
creatorSiteId: genesis.creatorSiteId,
|
||||
created: genesis.created,
|
||||
parameters: genesis.parameters,
|
||||
genesisHash: createHash('sha256')
|
||||
.update(JSON.stringify(genesisData))
|
||||
.digest('hex')
|
||||
.slice(0, 32),
|
||||
joined: true,
|
||||
isOwner: true,
|
||||
stats: { nodes: 1, contributors: 0, credits: 0 },
|
||||
};
|
||||
|
||||
this.networks.set(networkId, networkEntry);
|
||||
await this.save();
|
||||
|
||||
// Generate invite codes if private
|
||||
let inviteCodes = null;
|
||||
if (genesis.type !== NetworkType.PUBLIC) {
|
||||
inviteCodes = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
inviteCodes.push(genesis.generateInviteCode());
|
||||
}
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'invites.json'),
|
||||
JSON.stringify(inviteCodes, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
return { networkId, genesis: genesisData, inviteCodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Join an existing network
|
||||
*/
|
||||
async joinNetwork(networkId, inviteCode = null) {
|
||||
const network = this.networks.get(networkId);
|
||||
|
||||
if (!network) {
|
||||
throw new Error(`Network not found: ${networkId}`);
|
||||
}
|
||||
|
||||
if (network.joined) {
|
||||
return { alreadyJoined: true, network };
|
||||
}
|
||||
|
||||
// Verify invite code for private networks
|
||||
if (network.type === NetworkType.PRIVATE) {
|
||||
if (!inviteCode) {
|
||||
throw new Error('Private network requires invite code');
|
||||
}
|
||||
|
||||
const isValid = await this.verifyInviteCode(networkId, inviteCode);
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid or expired invite code');
|
||||
}
|
||||
}
|
||||
|
||||
// Create local network directory
|
||||
const networkDir = getNetworkDir(networkId);
|
||||
|
||||
// For well-known networks, create initial structure
|
||||
if (network.isWellKnown) {
|
||||
const qdag = {
|
||||
networkId,
|
||||
nodes: [{
|
||||
id: 'genesis',
|
||||
type: 'genesis',
|
||||
timestamp: Date.now(),
|
||||
message: `Joined: ${network.name}`,
|
||||
parents: [],
|
||||
weight: 1,
|
||||
confirmations: 0,
|
||||
}],
|
||||
tips: ['genesis'],
|
||||
confirmed: ['genesis'],
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'qdag.json'),
|
||||
JSON.stringify(qdag, null, 2)
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
join(networkDir, 'peers.json'),
|
||||
JSON.stringify([], null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
network.joined = true;
|
||||
network.joinedAt = new Date().toISOString();
|
||||
await this.save();
|
||||
|
||||
return { joined: true, network };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify invite code
|
||||
*/
|
||||
async verifyInviteCode(networkId, code) {
|
||||
try {
|
||||
const decoded = Buffer.from(code, 'base64url').toString();
|
||||
const [codeNetworkId, secret] = decoded.split(':');
|
||||
|
||||
if (codeNetworkId !== networkId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In production, verify against network's invite registry
|
||||
// For local simulation, accept any properly formatted code
|
||||
return secret && secret.length === 32;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover networks from DHT/registry
|
||||
*/
|
||||
async discoverNetworks(options = {}) {
|
||||
const discovered = [];
|
||||
|
||||
// Always include well-known networks
|
||||
for (const network of WELL_KNOWN_NETWORKS) {
|
||||
const existing = this.networks.get(network.id);
|
||||
discovered.push({
|
||||
...network,
|
||||
joined: existing?.joined || false,
|
||||
source: 'well-known',
|
||||
});
|
||||
}
|
||||
|
||||
// Scan for locally known networks
|
||||
try {
|
||||
const networksDir = getNetworksDir();
|
||||
const dirs = await fs.readdir(networksDir);
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir === 'registry.json') continue;
|
||||
|
||||
const genesisPath = join(networksDir, dir, 'genesis.json');
|
||||
if (existsSync(genesisPath)) {
|
||||
try {
|
||||
const genesis = JSON.parse(await fs.readFile(genesisPath, 'utf-8'));
|
||||
const existing = this.networks.get(genesis.networkId || dir);
|
||||
|
||||
if (!existing?.isWellKnown) {
|
||||
discovered.push({
|
||||
id: genesis.networkId || dir,
|
||||
name: genesis.name,
|
||||
description: genesis.description,
|
||||
type: genesis.type,
|
||||
creator: genesis.creatorSiteId,
|
||||
created: genesis.created,
|
||||
joined: existing?.joined || false,
|
||||
source: 'local',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid genesis files
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Networks directory doesn't exist yet
|
||||
}
|
||||
|
||||
// In production: Query DHT/bootstrap nodes for public networks
|
||||
// This is simulated here
|
||||
|
||||
return discovered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active network for contributions
|
||||
*/
|
||||
async setActiveNetwork(networkId) {
|
||||
const network = this.networks.get(networkId);
|
||||
|
||||
if (!network) {
|
||||
throw new Error(`Network not found: ${networkId}`);
|
||||
}
|
||||
|
||||
if (!network.joined) {
|
||||
throw new Error(`Must join network first: ${networkId}`);
|
||||
}
|
||||
|
||||
this.activeNetwork = networkId;
|
||||
await this.save();
|
||||
|
||||
return network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network info
|
||||
*/
|
||||
getNetwork(networkId) {
|
||||
return this.networks.get(networkId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active network
|
||||
*/
|
||||
getActiveNetwork() {
|
||||
if (!this.activeNetwork) return null;
|
||||
return this.networks.get(this.activeNetwork);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all joined networks
|
||||
*/
|
||||
getJoinedNetworks() {
|
||||
return Array.from(this.networks.values()).filter(n => n.joined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network statistics
|
||||
*/
|
||||
async getNetworkStats(networkId) {
|
||||
const networkDir = getNetworkDir(networkId);
|
||||
const qdagPath = join(networkDir, 'qdag.json');
|
||||
const peersPath = join(networkDir, 'peers.json');
|
||||
|
||||
const stats = {
|
||||
nodes: 0,
|
||||
contributions: 0,
|
||||
contributors: 0,
|
||||
credits: 0,
|
||||
peers: 0,
|
||||
};
|
||||
|
||||
try {
|
||||
if (existsSync(qdagPath)) {
|
||||
const qdag = JSON.parse(await fs.readFile(qdagPath, 'utf-8'));
|
||||
const contributions = (qdag.nodes || []).filter(n => n.type === 'contribution');
|
||||
|
||||
stats.nodes = qdag.nodes?.length || 0;
|
||||
stats.contributions = contributions.length;
|
||||
stats.contributors = new Set(contributions.map(c => c.contributor)).size;
|
||||
stats.credits = contributions.reduce((sum, c) => sum + (c.credits || 0), 0);
|
||||
}
|
||||
|
||||
if (existsSync(peersPath)) {
|
||||
const peers = JSON.parse(await fs.readFile(peersPath, 'utf-8'));
|
||||
stats.peers = peers.length;
|
||||
}
|
||||
} catch (err) {
|
||||
// Stats not available
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all networks
|
||||
*/
|
||||
listNetworks() {
|
||||
return Array.from(this.networks.values());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-Network Manager - coordinates contributions across networks
|
||||
*/
|
||||
export class MultiNetworkManager {
|
||||
constructor(identity) {
|
||||
this.identity = identity;
|
||||
this.registry = new NetworkRegistry();
|
||||
this.activeConnections = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.registry.load();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new network
|
||||
*/
|
||||
async createNetwork(options) {
|
||||
console.log(`\n${c('cyan', 'Creating new network...')}\n`);
|
||||
|
||||
const result = await this.registry.createNetwork(options, this.identity);
|
||||
|
||||
console.log(`${c('green', '✓')} Network created successfully!`);
|
||||
console.log(` ${c('cyan', 'Network ID:')} ${result.networkId}`);
|
||||
console.log(` ${c('cyan', 'Name:')} ${options.name}`);
|
||||
console.log(` ${c('cyan', 'Type:')} ${options.type}`);
|
||||
console.log(` ${c('cyan', 'Description:')} ${options.description || 'N/A'}`);
|
||||
|
||||
if (result.inviteCodes) {
|
||||
console.log(`\n${c('bold', 'Invite Codes (share these to invite members):')}`);
|
||||
for (const invite of result.inviteCodes.slice(0, 3)) {
|
||||
console.log(` ${c('yellow', invite.code)}`);
|
||||
}
|
||||
console.log(` ${c('dim', `(${result.inviteCodes.length} codes saved to network directory)`)}`);
|
||||
}
|
||||
|
||||
console.log(`\n${c('dim', 'Network directory:')} ~/.ruvector/networks/${result.networkId}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover available networks
|
||||
*/
|
||||
async discoverNetworks() {
|
||||
console.log(`\n${c('cyan', 'Discovering networks...')}\n`);
|
||||
|
||||
const networks = await this.registry.discoverNetworks();
|
||||
|
||||
if (networks.length === 0) {
|
||||
console.log(` ${c('dim', 'No networks found.')}`);
|
||||
return networks;
|
||||
}
|
||||
|
||||
console.log(`${c('bold', 'Available Networks:')}\n`);
|
||||
|
||||
for (const network of networks) {
|
||||
const status = network.joined ? c('green', '● Joined') : c('dim', '○ Not joined');
|
||||
const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
|
||||
network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
|
||||
|
||||
console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
|
||||
console.log(` ${c('dim', 'ID:')} ${network.id}`);
|
||||
console.log(` ${c('dim', 'Type:')} ${network.type}`);
|
||||
console.log(` ${c('dim', 'Description:')} ${network.description || 'N/A'}`);
|
||||
console.log(` ${c('dim', 'Source:')} ${network.source}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
return networks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a network
|
||||
*/
|
||||
async joinNetwork(networkId, inviteCode = null) {
|
||||
console.log(`\n${c('cyan', `Joining network ${networkId}...`)}\n`);
|
||||
|
||||
try {
|
||||
const result = await this.registry.joinNetwork(networkId, inviteCode);
|
||||
|
||||
if (result.alreadyJoined) {
|
||||
console.log(`${c('yellow', '⚠')} Already joined network: ${result.network.name}`);
|
||||
} else {
|
||||
console.log(`${c('green', '✓')} Successfully joined: ${result.network.name}`);
|
||||
}
|
||||
|
||||
// Set as active if it's the only joined network
|
||||
const joinedNetworks = this.registry.getJoinedNetworks();
|
||||
if (joinedNetworks.length === 1) {
|
||||
await this.registry.setActiveNetwork(networkId);
|
||||
console.log(` ${c('dim', 'Set as active network')}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.log(`${c('red', '✗')} Failed to join: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch active network
|
||||
*/
|
||||
async switchNetwork(networkId) {
|
||||
const network = await this.registry.setActiveNetwork(networkId);
|
||||
console.log(`${c('green', '✓')} Active network: ${network.name} (${networkId})`);
|
||||
return network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show network status
|
||||
*/
|
||||
async showStatus() {
|
||||
const active = this.registry.getActiveNetwork();
|
||||
const joined = this.registry.getJoinedNetworks();
|
||||
|
||||
console.log(`\n${c('bold', 'NETWORK STATUS:')}\n`);
|
||||
|
||||
if (!active) {
|
||||
console.log(` ${c('yellow', '⚠')} No active network`);
|
||||
console.log(` ${c('dim', 'Join a network to start contributing')}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await this.registry.getNetworkStats(active.id);
|
||||
|
||||
console.log(`${c('bold', 'Active Network:')}`);
|
||||
console.log(` ${c('cyan', 'Name:')} ${active.name}`);
|
||||
console.log(` ${c('cyan', 'ID:')} ${active.id}`);
|
||||
console.log(` ${c('cyan', 'Type:')} ${active.type}`);
|
||||
console.log(` ${c('cyan', 'QDAG Nodes:')} ${stats.nodes}`);
|
||||
console.log(` ${c('cyan', 'Contributions:')} ${stats.contributions}`);
|
||||
console.log(` ${c('cyan', 'Contributors:')} ${stats.contributors}`);
|
||||
console.log(` ${c('cyan', 'Total Credits:')} ${stats.credits}`);
|
||||
console.log(` ${c('cyan', 'Connected Peers:')} ${stats.peers}`);
|
||||
|
||||
if (joined.length > 1) {
|
||||
console.log(`\n${c('bold', 'Other Joined Networks:')}`);
|
||||
for (const network of joined) {
|
||||
if (network.id !== active.id) {
|
||||
console.log(` ${c('dim', '○')} ${network.name} (${network.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active network directory for contributions
|
||||
*/
|
||||
getActiveNetworkDir() {
|
||||
const active = this.registry.getActiveNetwork();
|
||||
if (!active) return null;
|
||||
return getNetworkDir(active.id);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
|
||||
const registry = new NetworkRegistry();
|
||||
await registry.load();
|
||||
|
||||
if (command === 'list' || command === 'ls') {
|
||||
console.log(`\n${c('bold', 'NETWORKS:')}\n`);
|
||||
|
||||
const networks = registry.listNetworks();
|
||||
const active = registry.activeNetwork;
|
||||
|
||||
for (const network of networks) {
|
||||
const isActive = network.id === active;
|
||||
const status = network.joined ?
|
||||
(isActive ? c('green', '● Active') : c('cyan', '○ Joined')) :
|
||||
c('dim', ' Available');
|
||||
const typeIcon = network.type === NetworkType.PUBLIC ? '🌐' :
|
||||
network.type === NetworkType.PRIVATE ? '🔒' : '🏢';
|
||||
|
||||
console.log(` ${status} ${typeIcon} ${c('bold', network.name)}`);
|
||||
console.log(` ${c('dim', 'ID:')} ${network.id}`);
|
||||
if (network.description) {
|
||||
console.log(` ${c('dim', network.description)}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} else if (command === 'discover') {
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.discoverNetworks();
|
||||
|
||||
} else if (command === 'create') {
|
||||
const name = args[1] || 'My Network';
|
||||
const type = args.includes('--private') ? NetworkType.PRIVATE :
|
||||
args.includes('--consortium') ? NetworkType.CONSORTIUM :
|
||||
NetworkType.PUBLIC;
|
||||
const description = args.find((a, i) => args[i - 1] === '--desc') || '';
|
||||
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.createNetwork({ name, type, description });
|
||||
|
||||
} else if (command === 'join') {
|
||||
const networkId = args[1];
|
||||
const inviteCode = args.find((a, i) => args[i - 1] === '--invite');
|
||||
|
||||
if (!networkId) {
|
||||
console.log(`${c('red', '✗')} Usage: networks join <network-id> [--invite <code>]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.joinNetwork(networkId, inviteCode);
|
||||
|
||||
} else if (command === 'switch' || command === 'use') {
|
||||
const networkId = args[1];
|
||||
|
||||
if (!networkId) {
|
||||
console.log(`${c('red', '✗')} Usage: networks switch <network-id>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.switchNetwork(networkId);
|
||||
|
||||
} else if (command === 'status') {
|
||||
const manager = new MultiNetworkManager(null);
|
||||
await manager.initialize();
|
||||
await manager.showStatus();
|
||||
|
||||
} else if (command === 'help' || !command) {
|
||||
console.log(`
|
||||
${c('bold', 'Edge-Net Multi-Network Manager')}
|
||||
|
||||
${c('bold', 'COMMANDS:')}
|
||||
${c('green', 'list')} List all known networks
|
||||
${c('green', 'discover')} Discover available networks
|
||||
${c('green', 'create')} Create a new network
|
||||
${c('green', 'join')} Join an existing network
|
||||
${c('green', 'switch')} Switch active network
|
||||
${c('green', 'status')} Show current network status
|
||||
${c('green', 'help')} Show this help
|
||||
|
||||
${c('bold', 'EXAMPLES:')}
|
||||
${c('dim', '# List networks')}
|
||||
$ node networks.js list
|
||||
|
||||
${c('dim', '# Create a public network')}
|
||||
$ node networks.js create "My Research Network" --desc "For ML research"
|
||||
|
||||
${c('dim', '# Create a private network')}
|
||||
$ node networks.js create "Team Network" --private
|
||||
|
||||
${c('dim', '# Join a network')}
|
||||
$ node networks.js join net-abc123def456
|
||||
|
||||
${c('dim', '# Join a private network with invite')}
|
||||
$ node networks.js join net-xyz789 --invite <invite-code>
|
||||
|
||||
${c('dim', '# Switch active network')}
|
||||
$ node networks.js switch net-abc123def456
|
||||
|
||||
${c('bold', 'NETWORK TYPES:')}
|
||||
${c('cyan', '🌐 Public')} Anyone can join and discover
|
||||
${c('cyan', '🔒 Private')} Requires invite code to join
|
||||
${c('cyan', '🏢 Consortium')} Requires approval from members
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
|
@ -52,6 +52,7 @@
|
|||
"join.js",
|
||||
"join.html",
|
||||
"network.js",
|
||||
"networks.js",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue