mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-22 19:56:25 +00:00
feat(edge-net): Add long-term persistence for multi-contributor network
- Implement PersistentIdentity class for months/years persistence - Store identities in ~/.ruvector/identities with encrypted backup - Track contribution history in ~/.ruvector/contributions - Add --list command to show all stored identities - Add --history command to show contribution milestones - Auto-restore identities across sessions - Track "return after absence" milestones (>30 days) - Session tracking with timestamps - Add multi-contributor-test.js for network simulation - All contributions preserved indefinitely
This commit is contained in:
parent
9df86fdcd8
commit
bd67b26e11
2 changed files with 949 additions and 12 deletions
|
|
@ -13,7 +13,7 @@
|
|||
* npx @ruvector/edge-net join --import <file> # Import identity from backup
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { webcrypto } from 'crypto';
|
||||
|
|
@ -113,6 +113,8 @@ ${c('bold', 'OPTIONS:')}
|
|||
${c('yellow', '--import <file>')} Import identity from encrypted backup
|
||||
${c('yellow', '--password <pw>')} Password for import/export operations
|
||||
${c('yellow', '--status')} Show current contributor status
|
||||
${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
|
||||
|
||||
|
|
@ -152,7 +154,7 @@ ${c('dim', 'Documentation: https://github.com/ruvnet/ruvector/tree/main/examples
|
|||
`);
|
||||
}
|
||||
|
||||
// Config directory for storing identities
|
||||
// Config directory for storing identities - persistent across months/years
|
||||
function getConfigDir() {
|
||||
const configDir = join(homedir(), '.ruvector');
|
||||
if (!existsSync(configDir)) {
|
||||
|
|
@ -161,6 +163,229 @@ function getConfigDir() {
|
|||
return configDir;
|
||||
}
|
||||
|
||||
function getIdentitiesDir() {
|
||||
const identitiesDir = join(getConfigDir(), 'identities');
|
||||
if (!existsSync(identitiesDir)) {
|
||||
mkdirSync(identitiesDir, { recursive: true });
|
||||
}
|
||||
return identitiesDir;
|
||||
}
|
||||
|
||||
function getContributionsDir() {
|
||||
const contribDir = join(getConfigDir(), 'contributions');
|
||||
if (!existsSync(contribDir)) {
|
||||
mkdirSync(contribDir, { recursive: true });
|
||||
}
|
||||
return contribDir;
|
||||
}
|
||||
|
||||
// Long-term persistent identity management
|
||||
class PersistentIdentity {
|
||||
constructor(siteId, wasm) {
|
||||
this.siteId = siteId;
|
||||
this.wasm = wasm;
|
||||
this.identityPath = join(getIdentitiesDir(), `${siteId}.identity`);
|
||||
this.metaPath = join(getIdentitiesDir(), `${siteId}.meta.json`);
|
||||
this.contributionPath = join(getContributionsDir(), `${siteId}.history.json`);
|
||||
this.piKey = null;
|
||||
this.meta = null;
|
||||
}
|
||||
|
||||
exists() {
|
||||
return existsSync(this.identityPath);
|
||||
}
|
||||
|
||||
// Generate new or restore existing identity
|
||||
async initialize(password) {
|
||||
if (this.exists()) {
|
||||
return this.restore(password);
|
||||
} else {
|
||||
return this.generate(password);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new identity with full metadata
|
||||
generate(password) {
|
||||
this.piKey = new this.wasm.PiKey();
|
||||
|
||||
// Save encrypted identity
|
||||
const backup = this.piKey.createEncryptedBackup(password);
|
||||
writeFileSync(this.identityPath, Buffer.from(backup));
|
||||
|
||||
// Save metadata (not secret)
|
||||
this.meta = {
|
||||
version: 1,
|
||||
siteId: this.siteId,
|
||||
shortId: this.piKey.getShortId(),
|
||||
publicKey: toHex(this.piKey.getPublicKey()),
|
||||
genesisFingerprint: toHex(this.piKey.getGenesisFingerprint()),
|
||||
createdAt: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString(),
|
||||
totalSessions: 1,
|
||||
totalContributions: 0
|
||||
};
|
||||
writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
|
||||
|
||||
// Initialize contribution history
|
||||
const history = {
|
||||
siteId: this.siteId,
|
||||
shortId: this.meta.shortId,
|
||||
sessions: [{
|
||||
started: new Date().toISOString(),
|
||||
type: 'genesis'
|
||||
}],
|
||||
contributions: [],
|
||||
milestones: [{
|
||||
type: 'identity_created',
|
||||
timestamp: new Date().toISOString()
|
||||
}]
|
||||
};
|
||||
writeFileSync(this.contributionPath, JSON.stringify(history, null, 2));
|
||||
|
||||
return { isNew: true, meta: this.meta };
|
||||
}
|
||||
|
||||
// Restore existing identity
|
||||
restore(password) {
|
||||
const backup = new Uint8Array(readFileSync(this.identityPath));
|
||||
this.piKey = this.wasm.PiKey.restoreFromBackup(backup, password);
|
||||
|
||||
// Load and update metadata
|
||||
if (existsSync(this.metaPath)) {
|
||||
this.meta = JSON.parse(readFileSync(this.metaPath, 'utf-8'));
|
||||
} else {
|
||||
// Rebuild metadata from key
|
||||
this.meta = {
|
||||
version: 1,
|
||||
siteId: this.siteId,
|
||||
shortId: this.piKey.getShortId(),
|
||||
publicKey: toHex(this.piKey.getPublicKey()),
|
||||
genesisFingerprint: toHex(this.piKey.getGenesisFingerprint()),
|
||||
createdAt: 'unknown',
|
||||
lastUsed: new Date().toISOString(),
|
||||
totalSessions: 1,
|
||||
totalContributions: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Update usage stats
|
||||
this.meta.lastUsed = new Date().toISOString();
|
||||
this.meta.totalSessions = (this.meta.totalSessions || 0) + 1;
|
||||
writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
|
||||
|
||||
// Update contribution history
|
||||
let history;
|
||||
if (existsSync(this.contributionPath)) {
|
||||
history = JSON.parse(readFileSync(this.contributionPath, 'utf-8'));
|
||||
} else {
|
||||
history = {
|
||||
siteId: this.siteId,
|
||||
shortId: this.meta.shortId,
|
||||
sessions: [],
|
||||
contributions: [],
|
||||
milestones: []
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate time since last session
|
||||
const lastSession = history.sessions[history.sessions.length - 1];
|
||||
let timeSinceLastSession = null;
|
||||
if (lastSession && lastSession.started) {
|
||||
const last = new Date(lastSession.started);
|
||||
const now = new Date();
|
||||
const diffMs = now - last;
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
timeSinceLastSession = diffDays;
|
||||
|
||||
if (diffDays > 30) {
|
||||
history.milestones.push({
|
||||
type: 'returned_after_absence',
|
||||
timestamp: new Date().toISOString(),
|
||||
daysSinceLastSession: diffDays
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
history.sessions.push({
|
||||
started: new Date().toISOString(),
|
||||
type: 'restored',
|
||||
timeSinceLastDays: timeSinceLastSession
|
||||
});
|
||||
|
||||
writeFileSync(this.contributionPath, JSON.stringify(history, null, 2));
|
||||
|
||||
return {
|
||||
isNew: false,
|
||||
meta: this.meta,
|
||||
sessions: this.meta.totalSessions,
|
||||
daysSinceLastSession: timeSinceLastSession
|
||||
};
|
||||
}
|
||||
|
||||
// Record a contribution
|
||||
recordContribution(type, details = {}) {
|
||||
this.meta.totalContributions = (this.meta.totalContributions || 0) + 1;
|
||||
this.meta.lastUsed = new Date().toISOString();
|
||||
writeFileSync(this.metaPath, JSON.stringify(this.meta, null, 2));
|
||||
|
||||
let history = { sessions: [], contributions: [], milestones: [] };
|
||||
if (existsSync(this.contributionPath)) {
|
||||
history = JSON.parse(readFileSync(this.contributionPath, 'utf-8'));
|
||||
}
|
||||
|
||||
history.contributions.push({
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
...details
|
||||
});
|
||||
|
||||
writeFileSync(this.contributionPath, JSON.stringify(history, null, 2));
|
||||
return this.meta.totalContributions;
|
||||
}
|
||||
|
||||
// Get full history
|
||||
getHistory() {
|
||||
if (!existsSync(this.contributionPath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(readFileSync(this.contributionPath, 'utf-8'));
|
||||
}
|
||||
|
||||
// Get public info for sharing
|
||||
getPublicInfo() {
|
||||
return {
|
||||
siteId: this.siteId,
|
||||
shortId: this.meta.shortId,
|
||||
publicKey: this.meta.publicKey,
|
||||
genesisFingerprint: this.meta.genesisFingerprint,
|
||||
memberSince: this.meta.createdAt,
|
||||
totalContributions: this.meta.totalContributions
|
||||
};
|
||||
}
|
||||
|
||||
free() {
|
||||
if (this.piKey) this.piKey.free();
|
||||
}
|
||||
}
|
||||
|
||||
// List all stored identities
|
||||
function listStoredIdentities() {
|
||||
const identitiesDir = getIdentitiesDir();
|
||||
if (!existsSync(identitiesDir)) return [];
|
||||
|
||||
const files = readdirSync(identitiesDir);
|
||||
const identities = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.meta.json')) {
|
||||
const meta = JSON.parse(readFileSync(join(identitiesDir, file), 'utf-8'));
|
||||
identities.push(meta);
|
||||
}
|
||||
}
|
||||
|
||||
return identities;
|
||||
}
|
||||
|
||||
function toHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
|
@ -183,6 +408,8 @@ function parseArgs(args) {
|
|||
import: null,
|
||||
password: null,
|
||||
status: false,
|
||||
history: false,
|
||||
list: false,
|
||||
peers: false,
|
||||
help: false,
|
||||
};
|
||||
|
|
@ -211,6 +438,12 @@ function parseArgs(args) {
|
|||
case '--status':
|
||||
opts.status = true;
|
||||
break;
|
||||
case '--history':
|
||||
opts.history = true;
|
||||
break;
|
||||
case '--list':
|
||||
opts.list = true;
|
||||
break;
|
||||
case '--peers':
|
||||
opts.peers = true;
|
||||
break;
|
||||
|
|
@ -224,6 +457,85 @@ function parseArgs(args) {
|
|||
return opts;
|
||||
}
|
||||
|
||||
// Show contribution history
|
||||
async function showHistory(wasm, siteId, password) {
|
||||
console.log(`${c('bold', 'CONTRIBUTION HISTORY:')}\n`);
|
||||
|
||||
const identity = new PersistentIdentity(siteId, wasm);
|
||||
|
||||
if (!identity.exists()) {
|
||||
console.log(`${c('yellow', '⚠')} No identity found for site "${siteId}"`);
|
||||
console.log(`${c('dim', 'Run without --history to create one.')}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
await identity.initialize(password);
|
||||
const history = identity.getHistory();
|
||||
|
||||
if (!history) {
|
||||
console.log(`${c('dim', 'No history available.')}\n`);
|
||||
identity.free();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` ${c('cyan', 'Site ID:')} ${history.siteId}`);
|
||||
console.log(` ${c('cyan', 'Short ID:')} ${history.shortId}`);
|
||||
console.log(` ${c('cyan', 'Sessions:')} ${history.sessions.length}`);
|
||||
console.log(` ${c('cyan', 'Contributions:')} ${history.contributions.length}`);
|
||||
console.log(` ${c('cyan', 'Milestones:')} ${history.milestones.length}\n`);
|
||||
|
||||
if (history.milestones.length > 0) {
|
||||
console.log(` ${c('bold', 'Milestones:')}`);
|
||||
history.milestones.slice(-5).forEach(m => {
|
||||
const date = new Date(m.timestamp).toLocaleDateString();
|
||||
console.log(` ${c('dim', date)} - ${c('green', m.type)}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (history.sessions.length > 0) {
|
||||
console.log(`\n ${c('bold', 'Recent Sessions:')}`);
|
||||
history.sessions.slice(-5).forEach(s => {
|
||||
const date = new Date(s.started).toLocaleDateString();
|
||||
const time = new Date(s.started).toLocaleTimeString();
|
||||
const elapsed = s.timeSinceLastDays ? ` (${s.timeSinceLastDays}d since last)` : '';
|
||||
console.log(` ${c('dim', date + ' ' + time)} - ${s.type}${elapsed}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
identity.free();
|
||||
}
|
||||
|
||||
// List all stored identities
|
||||
async function listIdentities() {
|
||||
console.log(`${c('bold', 'STORED IDENTITIES:')}\n`);
|
||||
|
||||
const identities = listStoredIdentities();
|
||||
|
||||
if (identities.length === 0) {
|
||||
console.log(` ${c('dim', 'No identities found.')}`);
|
||||
console.log(` ${c('dim', 'Run "npx @ruvector/edge-net join" to create one.')}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` ${c('cyan', 'Found')} ${identities.length} ${c('cyan', 'identities:')}\n`);
|
||||
|
||||
for (const meta of identities) {
|
||||
const memberSince = meta.createdAt ? new Date(meta.createdAt).toLocaleDateString() : 'unknown';
|
||||
const lastUsed = meta.lastUsed ? new Date(meta.lastUsed).toLocaleDateString() : 'unknown';
|
||||
|
||||
console.log(` ${c('bold', meta.siteId)}`);
|
||||
console.log(` ${c('dim', 'ID:')} ${meta.shortId}`);
|
||||
console.log(` ${c('dim', 'Public Key:')} ${meta.publicKey.substring(0, 16)}...`);
|
||||
console.log(` ${c('dim', 'Member Since:')} ${memberSince}`);
|
||||
console.log(` ${c('dim', 'Last Used:')} ${lastUsed}`);
|
||||
console.log(` ${c('dim', 'Sessions:')} ${meta.totalSessions || 0}`);
|
||||
console.log(` ${c('dim', 'Contributions:')} ${meta.totalContributions || 0}\n`);
|
||||
}
|
||||
|
||||
console.log(`${c('dim', 'Storage: ' + getIdentitiesDir())}\n`);
|
||||
}
|
||||
|
||||
async function generateIdentity(wasm, siteId) {
|
||||
console.log(`${c('cyan', 'Generating new Pi-Key identity...')}\n`);
|
||||
|
||||
|
|
@ -483,6 +795,13 @@ async function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle --list early (no WASM needed)
|
||||
if (opts.list) {
|
||||
printBanner();
|
||||
await listIdentities();
|
||||
return;
|
||||
}
|
||||
|
||||
printBanner();
|
||||
await setupPolyfills();
|
||||
|
||||
|
|
@ -494,7 +813,15 @@ async function main() {
|
|||
const wasm = require('./node/ruvector_edge_net.cjs');
|
||||
console.log(`${c('green', '✓')} WASM module loaded\n`);
|
||||
|
||||
// Handle --history
|
||||
if (opts.history) {
|
||||
const password = opts.password || `${opts.site}-edge-net-key`;
|
||||
await showHistory(wasm, opts.site, password);
|
||||
return;
|
||||
}
|
||||
|
||||
let piKey = null;
|
||||
let persistentIdentity = null;
|
||||
|
||||
try {
|
||||
// Handle different modes
|
||||
|
|
@ -511,38 +838,148 @@ async function main() {
|
|||
console.log(`${c('dim', 'Note: Full key management requires import/export.')}\n`);
|
||||
piKey = new wasm.PiKey();
|
||||
} else {
|
||||
// Generate new identity
|
||||
const result = await generateIdentity(wasm, opts.site);
|
||||
piKey = result.piKey;
|
||||
// Use persistent identity (auto-creates or restores)
|
||||
const password = opts.password || `${opts.site}-edge-net-key`;
|
||||
persistentIdentity = new PersistentIdentity(opts.site, wasm);
|
||||
const result = await persistentIdentity.initialize(password);
|
||||
|
||||
if (result.isNew) {
|
||||
console.log(`${c('green', '✓')} New identity created: ${result.meta.shortId}`);
|
||||
console.log(` ${c('dim', 'Your identity is now stored locally and will persist.')}`);
|
||||
console.log(` ${c('dim', 'Storage:')} ${getIdentitiesDir()}\n`);
|
||||
} else {
|
||||
console.log(`${c('green', '✓')} Identity restored: ${result.meta.shortId}`);
|
||||
console.log(` ${c('dim', 'Member since:')} ${result.meta.createdAt}`);
|
||||
console.log(` ${c('dim', 'Total sessions:')} ${result.sessions}`);
|
||||
if (result.daysSinceLastSession !== null) {
|
||||
if (result.daysSinceLastSession > 30) {
|
||||
console.log(` ${c('yellow', 'Welcome back!')} ${result.daysSinceLastSession} days since last session`);
|
||||
} else if (result.daysSinceLastSession > 0) {
|
||||
console.log(` ${c('dim', 'Last session:')} ${result.daysSinceLastSession} days ago`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
piKey = persistentIdentity.piKey;
|
||||
}
|
||||
|
||||
if (opts.generate) {
|
||||
// Just generate, don't join
|
||||
console.log(`${c('green', '✓ Identity generated successfully!')}\n`);
|
||||
console.log(`${c('dim', 'Use --export to save, or run without --generate to join.')}\n`);
|
||||
console.log(`${c('green', '✓ Identity generated and persisted!')}\n`);
|
||||
console.log(`${c('dim', 'Your identity is stored at:')} ${getIdentitiesDir()}`);
|
||||
console.log(`${c('dim', 'Run again to continue with the same identity.')}\n`);
|
||||
|
||||
// Also demonstrate multi-contributor
|
||||
piKey.free();
|
||||
if (persistentIdentity) persistentIdentity.free();
|
||||
else if (piKey) piKey.free();
|
||||
await demonstrateMultiContributor(wasm);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.status) {
|
||||
await showStatus(wasm, piKey);
|
||||
piKey.free();
|
||||
if (persistentIdentity) persistentIdentity.free();
|
||||
else if (piKey) piKey.free();
|
||||
return;
|
||||
}
|
||||
|
||||
// Join the network
|
||||
await joinNetwork(wasm, opts, piKey);
|
||||
// Join the network with persistence
|
||||
if (persistentIdentity) {
|
||||
await joinNetworkPersistent(wasm, opts, persistentIdentity);
|
||||
} else {
|
||||
await joinNetwork(wasm, opts, piKey);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`${c('red', '✗ Error:')} ${err.message}`);
|
||||
if (piKey) piKey.free();
|
||||
if (persistentIdentity) persistentIdentity.free();
|
||||
else if (piKey) piKey.free();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Join network with persistent identity (tracks contributions)
|
||||
async function joinNetworkPersistent(wasm, opts, identity) {
|
||||
console.log(`${c('bold', 'JOINING EDGE-NET (Persistent Mode)...')}\n`);
|
||||
|
||||
const publicKeyHex = identity.meta.publicKey;
|
||||
|
||||
// Create components for network participation
|
||||
const detector = new wasm.ByzantineDetector(0.5);
|
||||
const dp = new wasm.DifferentialPrivacy(1.0, 0.001);
|
||||
const model = new wasm.FederatedModel(100, 0.01, 0.9);
|
||||
const coherence = new wasm.CoherenceEngine();
|
||||
const evolution = new wasm.EvolutionEngine();
|
||||
const events = new wasm.NetworkEvents();
|
||||
|
||||
console.log(`${c('bold', 'CONTRIBUTOR NODE:')}`);
|
||||
console.log(` ${c('cyan', 'Site ID:')} ${opts.site}`);
|
||||
console.log(` ${c('cyan', 'Short ID:')} ${identity.meta.shortId}`);
|
||||
console.log(` ${c('cyan', 'Public Key:')} ${publicKeyHex.substring(0, 16)}...${publicKeyHex.slice(-8)}`);
|
||||
console.log(` ${c('cyan', 'Member Since:')} ${new Date(identity.meta.createdAt).toLocaleDateString()}`);
|
||||
console.log(` ${c('cyan', 'Sessions:')} ${identity.meta.totalSessions}`);
|
||||
console.log(` ${c('cyan', 'Status:')} ${c('green', 'Connected')}`);
|
||||
console.log(` ${c('cyan', 'Mode:')} Persistent\n`);
|
||||
|
||||
console.log(`${c('bold', 'ACTIVE COMPONENTS:')}`);
|
||||
console.log(` ${c('green', '✓')} Byzantine Detector (threshold=0.5)`);
|
||||
console.log(` ${c('green', '✓')} Differential Privacy (ε=1.0)`);
|
||||
console.log(` ${c('green', '✓')} Federated Model (dim=100)`);
|
||||
console.log(` ${c('green', '✓')} Coherence Engine`);
|
||||
console.log(` ${c('green', '✓')} Evolution Engine`);
|
||||
|
||||
// Get themed status
|
||||
const themedStatus = events.getThemedStatus(1, BigInt(identity.meta.totalContributions || 0));
|
||||
console.log(`\n${c('bold', 'NETWORK STATUS:')}`);
|
||||
console.log(` ${themedStatus}\n`);
|
||||
|
||||
// Show persistence info
|
||||
console.log(`${c('bold', 'PERSISTENCE:')}`);
|
||||
console.log(` ${c('dim', 'Identity stored at:')} ${identity.identityPath}`);
|
||||
console.log(` ${c('dim', 'History stored at:')} ${identity.contributionPath}`);
|
||||
console.log(` ${c('dim', 'Your contributions are preserved across sessions (months/years).')}\n`);
|
||||
|
||||
console.log(`${c('green', '✓ Successfully joined Edge-Net!')}\n`);
|
||||
console.log(`${c('dim', 'Press Ctrl+C to disconnect.')}\n`);
|
||||
|
||||
// Keep running with periodic status updates and contribution tracking
|
||||
let ticks = 0;
|
||||
let contributions = 0;
|
||||
const statusInterval = setInterval(() => {
|
||||
ticks++;
|
||||
|
||||
// Simulate contribution every 5 seconds
|
||||
if (ticks % 5 === 0) {
|
||||
contributions++;
|
||||
identity.recordContribution('compute', { duration: 5, tick: ticks });
|
||||
}
|
||||
|
||||
const motivation = events.getMotivation(BigInt(ticks * 10));
|
||||
if (ticks % 10 === 0) {
|
||||
console.log(` ${c('dim', `[${ticks}s]`)} ${c('cyan', 'Contributing...')} ${contributions} total | ${motivation}`);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
clearInterval(statusInterval);
|
||||
console.log(`\n${c('yellow', 'Disconnected from Edge-Net.')}`);
|
||||
console.log(`${c('green', '✓')} Session recorded: ${contributions} contributions`);
|
||||
console.log(`${c('dim', 'Your identity and history are preserved. Rejoin anytime.')}\n`);
|
||||
|
||||
// Clean up WASM resources
|
||||
detector.free();
|
||||
dp.free();
|
||||
model.free();
|
||||
coherence.free();
|
||||
evolution.free();
|
||||
events.free();
|
||||
identity.free();
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(`${colors.red}Fatal error: ${err.message}${colors.reset}`);
|
||||
process.exit(1);
|
||||
|
|
|
|||
500
examples/edge-net/pkg/multi-contributor-test.js
Normal file
500
examples/edge-net/pkg/multi-contributor-test.js
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Multi-Contributor Edge-Net Test with Persistence
|
||||
*
|
||||
* Tests:
|
||||
* 1. Multiple contributors with persistent identities
|
||||
* 2. State persistence (patterns, ledger, coherence)
|
||||
* 3. Cross-contributor verification
|
||||
* 4. Session restore from persisted data
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { webcrypto } from 'crypto';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Setup polyfills
|
||||
async function setupPolyfills() {
|
||||
if (typeof globalThis.crypto === 'undefined') {
|
||||
globalThis.crypto = webcrypto;
|
||||
}
|
||||
if (typeof globalThis.performance === 'undefined') {
|
||||
globalThis.performance = performance;
|
||||
}
|
||||
|
||||
const createStorage = () => {
|
||||
const store = new Map();
|
||||
return {
|
||||
getItem: (key) => store.get(key) || null,
|
||||
setItem: (key, value) => store.set(key, String(value)),
|
||||
removeItem: (key) => store.delete(key),
|
||||
clear: () => store.clear(),
|
||||
get length() { return store.size; },
|
||||
key: (i) => [...store.keys()][i] || null,
|
||||
};
|
||||
};
|
||||
|
||||
let cpuCount = 4;
|
||||
try {
|
||||
const os = await import('os');
|
||||
cpuCount = os.cpus().length;
|
||||
} catch {}
|
||||
|
||||
if (typeof globalThis.window === 'undefined') {
|
||||
globalThis.window = {
|
||||
crypto: globalThis.crypto,
|
||||
performance: globalThis.performance,
|
||||
localStorage: createStorage(),
|
||||
sessionStorage: createStorage(),
|
||||
navigator: {
|
||||
userAgent: `Node.js/${process.version}`,
|
||||
hardwareConcurrency: cpuCount,
|
||||
},
|
||||
location: { href: 'node://localhost', hostname: 'localhost' },
|
||||
screen: { width: 1920, height: 1080, colorDepth: 24 },
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof globalThis.document === 'undefined') {
|
||||
globalThis.document = { createElement: () => ({}), body: {}, head: {} };
|
||||
}
|
||||
}
|
||||
|
||||
// Colors
|
||||
const c = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
cyan: '\x1b[36m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
magenta: '\x1b[35m',
|
||||
};
|
||||
|
||||
function toHex(bytes) {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// Storage directory
|
||||
const STORAGE_DIR = join(homedir(), '.ruvector', 'edge-net-test');
|
||||
|
||||
function ensureStorageDir() {
|
||||
if (!existsSync(STORAGE_DIR)) {
|
||||
mkdirSync(STORAGE_DIR, { recursive: true });
|
||||
}
|
||||
return STORAGE_DIR;
|
||||
}
|
||||
|
||||
// Contributor class with persistence
|
||||
class PersistentContributor {
|
||||
constructor(wasm, id, storageDir) {
|
||||
this.wasm = wasm;
|
||||
this.id = id;
|
||||
this.storageDir = storageDir;
|
||||
this.identityPath = join(storageDir, `contributor-${id}.identity`);
|
||||
this.statePath = join(storageDir, `contributor-${id}.state`);
|
||||
this.piKey = null;
|
||||
this.coherence = null;
|
||||
this.reasoning = null;
|
||||
this.memory = null;
|
||||
this.ledger = null;
|
||||
this.patterns = [];
|
||||
}
|
||||
|
||||
// Initialize or restore from persistence
|
||||
async initialize() {
|
||||
const password = `contributor-${this.id}-secret`;
|
||||
|
||||
// Try to restore identity
|
||||
if (existsSync(this.identityPath)) {
|
||||
console.log(` ${c.cyan}[${this.id}]${c.reset} Restoring identity from storage...`);
|
||||
const backup = new Uint8Array(readFileSync(this.identityPath));
|
||||
this.piKey = this.wasm.PiKey.restoreFromBackup(backup, password);
|
||||
console.log(` ${c.green}✓${c.reset} Identity restored: ${this.piKey.getShortId()}`);
|
||||
} else {
|
||||
console.log(` ${c.cyan}[${this.id}]${c.reset} Generating new identity...`);
|
||||
this.piKey = new this.wasm.PiKey();
|
||||
// Persist immediately
|
||||
const backup = this.piKey.createEncryptedBackup(password);
|
||||
writeFileSync(this.identityPath, Buffer.from(backup));
|
||||
console.log(` ${c.green}✓${c.reset} New identity created: ${this.piKey.getShortId()}`);
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
this.coherence = new this.wasm.CoherenceEngine();
|
||||
this.reasoning = new this.wasm.ReasoningBank();
|
||||
this.memory = new this.wasm.CollectiveMemory(this.getNodeId());
|
||||
this.ledger = new this.wasm.QDAGLedger();
|
||||
|
||||
// Try to restore state
|
||||
if (existsSync(this.statePath)) {
|
||||
console.log(` ${c.cyan}[${this.id}]${c.reset} Restoring state...`);
|
||||
const state = JSON.parse(readFileSync(this.statePath, 'utf-8'));
|
||||
|
||||
// Restore ledger state if available
|
||||
if (state.ledger) {
|
||||
const ledgerBytes = new Uint8Array(state.ledger);
|
||||
const imported = this.ledger.importState(ledgerBytes);
|
||||
console.log(` ${c.green}✓${c.reset} Ledger restored: ${imported} transactions`);
|
||||
}
|
||||
|
||||
// Restore patterns
|
||||
if (state.patterns) {
|
||||
this.patterns = state.patterns;
|
||||
state.patterns.forEach(p => this.reasoning.store(JSON.stringify(p)));
|
||||
console.log(` ${c.green}✓${c.reset} Patterns restored: ${state.patterns.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
getNodeId() {
|
||||
return `node-${this.id}-${this.piKey.getShortId()}`;
|
||||
}
|
||||
|
||||
getPublicKey() {
|
||||
return this.piKey.getPublicKey();
|
||||
}
|
||||
|
||||
// Sign data
|
||||
sign(data) {
|
||||
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
||||
return this.piKey.sign(bytes);
|
||||
}
|
||||
|
||||
// Verify signature from another contributor
|
||||
verify(data, signature, publicKey) {
|
||||
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
|
||||
return this.piKey.verify(bytes, signature, publicKey);
|
||||
}
|
||||
|
||||
// Store a pattern
|
||||
storePattern(pattern) {
|
||||
const id = this.reasoning.store(JSON.stringify(pattern));
|
||||
this.patterns.push(pattern);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Lookup patterns
|
||||
lookupPatterns(query, k = 3) {
|
||||
return JSON.parse(this.reasoning.lookup(JSON.stringify(query), k));
|
||||
}
|
||||
|
||||
// Get coherence stats
|
||||
getCoherenceStats() {
|
||||
return JSON.parse(this.coherence.getStats());
|
||||
}
|
||||
|
||||
// Get memory stats
|
||||
getMemoryStats() {
|
||||
return JSON.parse(this.memory.getStats());
|
||||
}
|
||||
|
||||
// Persist state
|
||||
persist() {
|
||||
const state = {
|
||||
timestamp: Date.now(),
|
||||
nodeId: this.getNodeId(),
|
||||
patterns: this.patterns,
|
||||
ledger: Array.from(this.ledger.exportState()),
|
||||
stats: {
|
||||
coherence: this.getCoherenceStats(),
|
||||
memory: this.getMemoryStats(),
|
||||
patternCount: this.reasoning.count(),
|
||||
txCount: this.ledger.transactionCount()
|
||||
}
|
||||
};
|
||||
|
||||
writeFileSync(this.statePath, JSON.stringify(state, null, 2));
|
||||
return state;
|
||||
}
|
||||
|
||||
// Cleanup WASM resources
|
||||
cleanup() {
|
||||
if (this.piKey) this.piKey.free();
|
||||
if (this.coherence) this.coherence.free();
|
||||
if (this.reasoning) this.reasoning.free();
|
||||
if (this.memory) this.memory.free();
|
||||
if (this.ledger) this.ledger.free();
|
||||
}
|
||||
}
|
||||
|
||||
// Network simulation
|
||||
class EdgeNetwork {
|
||||
constructor(wasm, storageDir) {
|
||||
this.wasm = wasm;
|
||||
this.storageDir = storageDir;
|
||||
this.contributors = new Map();
|
||||
this.sharedMessages = [];
|
||||
}
|
||||
|
||||
async addContributor(id) {
|
||||
const contributor = new PersistentContributor(this.wasm, id, this.storageDir);
|
||||
await contributor.initialize();
|
||||
this.contributors.set(id, contributor);
|
||||
return contributor;
|
||||
}
|
||||
|
||||
// Broadcast a signed message
|
||||
broadcastMessage(senderId, message) {
|
||||
const sender = this.contributors.get(senderId);
|
||||
const signature = sender.sign(message);
|
||||
|
||||
this.sharedMessages.push({
|
||||
from: senderId,
|
||||
message,
|
||||
signature: Array.from(signature),
|
||||
publicKey: Array.from(sender.getPublicKey()),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
// Verify all messages from network perspective
|
||||
verifyAllMessages() {
|
||||
const results = [];
|
||||
|
||||
for (const msg of this.sharedMessages) {
|
||||
const signature = new Uint8Array(msg.signature);
|
||||
const publicKey = new Uint8Array(msg.publicKey);
|
||||
|
||||
// Each contributor verifies
|
||||
for (const [id, contributor] of this.contributors) {
|
||||
if (id !== msg.from) {
|
||||
const valid = contributor.verify(msg.message, signature, publicKey);
|
||||
results.push({
|
||||
message: msg.message.substring(0, 30) + '...',
|
||||
from: msg.from,
|
||||
verifiedBy: id,
|
||||
valid
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Share patterns across network
|
||||
sharePatterns() {
|
||||
const allPatterns = [];
|
||||
|
||||
for (const [id, contributor] of this.contributors) {
|
||||
contributor.patterns.forEach(p => {
|
||||
allPatterns.push({ ...p, contributor: id });
|
||||
});
|
||||
}
|
||||
|
||||
return allPatterns;
|
||||
}
|
||||
|
||||
// Persist all contributors
|
||||
persistAll() {
|
||||
const states = {};
|
||||
for (const [id, contributor] of this.contributors) {
|
||||
states[id] = contributor.persist();
|
||||
}
|
||||
|
||||
// Save network state
|
||||
const networkState = {
|
||||
timestamp: Date.now(),
|
||||
contributors: Array.from(this.contributors.keys()),
|
||||
messages: this.sharedMessages,
|
||||
totalPatterns: this.sharePatterns().length
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
join(this.storageDir, 'network-state.json'),
|
||||
JSON.stringify(networkState, null, 2)
|
||||
);
|
||||
|
||||
return { states, networkState };
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
for (const [, contributor] of this.contributors) {
|
||||
contributor.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main test
|
||||
async function runMultiContributorTest() {
|
||||
console.log(`
|
||||
${c.cyan}╔═══════════════════════════════════════════════════════════════╗${c.reset}
|
||||
${c.cyan}║${c.reset} ${c.bold}Multi-Contributor Edge-Net Test with Persistence${c.reset} ${c.cyan}║${c.reset}
|
||||
${c.cyan}╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
||||
`);
|
||||
|
||||
await setupPolyfills();
|
||||
|
||||
// Load WASM
|
||||
const { createRequire } = await import('module');
|
||||
const require = createRequire(import.meta.url);
|
||||
console.log(`${c.dim}Loading WASM module...${c.reset}`);
|
||||
const wasm = require('./node/ruvector_edge_net.cjs');
|
||||
console.log(`${c.green}✓${c.reset} WASM module loaded\n`);
|
||||
|
||||
// Setup storage
|
||||
const storageDir = ensureStorageDir();
|
||||
console.log(`${c.cyan}Storage:${c.reset} ${storageDir}\n`);
|
||||
|
||||
// Check if this is a continuation
|
||||
const networkStatePath = join(storageDir, 'network-state.json');
|
||||
const isContinuation = existsSync(networkStatePath);
|
||||
|
||||
if (isContinuation) {
|
||||
const prevState = JSON.parse(readFileSync(networkStatePath, 'utf-8'));
|
||||
console.log(`${c.yellow}Continuing from previous session:${c.reset}`);
|
||||
console.log(` Previous timestamp: ${new Date(prevState.timestamp).toISOString()}`);
|
||||
console.log(` Contributors: ${prevState.contributors.join(', ')}`);
|
||||
console.log(` Messages: ${prevState.messages.length}`);
|
||||
console.log(` Patterns: ${prevState.totalPatterns}\n`);
|
||||
} else {
|
||||
console.log(`${c.green}Starting fresh network...${c.reset}\n`);
|
||||
}
|
||||
|
||||
// Create network
|
||||
const network = new EdgeNetwork(wasm, storageDir);
|
||||
|
||||
try {
|
||||
// ==== Phase 1: Initialize Contributors ====
|
||||
console.log(`${c.bold}=== Phase 1: Initialize Contributors ===${c.reset}\n`);
|
||||
|
||||
const contributorIds = ['alice', 'bob', 'charlie'];
|
||||
|
||||
for (const id of contributorIds) {
|
||||
await network.addContributor(id);
|
||||
}
|
||||
|
||||
console.log(`\n${c.green}✓${c.reset} ${network.contributors.size} contributors initialized\n`);
|
||||
|
||||
// ==== Phase 2: Cross-Verification ====
|
||||
console.log(`${c.bold}=== Phase 2: Cross-Verification ===${c.reset}\n`);
|
||||
|
||||
// Each contributor signs a message
|
||||
for (const id of contributorIds) {
|
||||
const message = `Hello from ${id} at ${Date.now()}`;
|
||||
network.broadcastMessage(id, message);
|
||||
console.log(` ${c.cyan}[${id}]${c.reset} Broadcast: "${message.substring(0, 40)}..."`);
|
||||
}
|
||||
|
||||
// Verify all signatures
|
||||
const verifications = network.verifyAllMessages();
|
||||
const allValid = verifications.every(v => v.valid);
|
||||
|
||||
console.log(`\n ${c.bold}Verification Results:${c.reset}`);
|
||||
verifications.forEach(v => {
|
||||
console.log(` ${v.valid ? c.green + '✓' : c.red + '✗'}${c.reset} ${v.from} → ${v.verifiedBy}`);
|
||||
});
|
||||
console.log(`\n${allValid ? c.green + '✓' : c.red + '✗'}${c.reset} All ${verifications.length} verifications ${allValid ? 'passed' : 'FAILED'}\n`);
|
||||
|
||||
// ==== Phase 3: Pattern Storage ====
|
||||
console.log(`${c.bold}=== Phase 3: Pattern Storage & Learning ===${c.reset}\n`);
|
||||
|
||||
// Each contributor stores some patterns
|
||||
const patternData = {
|
||||
alice: [
|
||||
{ centroid: [1.0, 0.0, 0.0], confidence: 0.95, task: 'compute' },
|
||||
{ centroid: [0.9, 0.1, 0.0], confidence: 0.88, task: 'inference' }
|
||||
],
|
||||
bob: [
|
||||
{ centroid: [0.0, 1.0, 0.0], confidence: 0.92, task: 'training' },
|
||||
{ centroid: [0.1, 0.9, 0.0], confidence: 0.85, task: 'validation' }
|
||||
],
|
||||
charlie: [
|
||||
{ centroid: [0.0, 0.0, 1.0], confidence: 0.90, task: 'storage' },
|
||||
{ centroid: [0.1, 0.1, 0.8], confidence: 0.87, task: 'retrieval' }
|
||||
]
|
||||
};
|
||||
|
||||
for (const [id, patterns] of Object.entries(patternData)) {
|
||||
const contributor = network.contributors.get(id);
|
||||
patterns.forEach(p => contributor.storePattern(p));
|
||||
console.log(` ${c.cyan}[${id}]${c.reset} Stored ${patterns.length} patterns`);
|
||||
}
|
||||
|
||||
// Lookup patterns
|
||||
console.log(`\n ${c.bold}Pattern Lookups:${c.reset}`);
|
||||
const alice = network.contributors.get('alice');
|
||||
const similar = alice.lookupPatterns([0.95, 0.05, 0.0], 2);
|
||||
console.log(` Alice searches for [0.95, 0.05, 0.0]: Found ${similar.length} similar patterns`);
|
||||
similar.forEach((p, i) => {
|
||||
console.log(` ${i + 1}. similarity=${p.similarity.toFixed(3)}, task=${p.pattern?.task || 'unknown'}`);
|
||||
});
|
||||
|
||||
const totalPatterns = network.sharePatterns();
|
||||
console.log(`\n${c.green}✓${c.reset} Total patterns in network: ${totalPatterns.length}\n`);
|
||||
|
||||
// ==== Phase 4: Coherence Check ====
|
||||
console.log(`${c.bold}=== Phase 4: Coherence State ===${c.reset}\n`);
|
||||
|
||||
for (const [id, contributor] of network.contributors) {
|
||||
const stats = contributor.getCoherenceStats();
|
||||
console.log(` ${c.cyan}[${id}]${c.reset} Merkle: ${contributor.coherence.getMerkleRoot().substring(0, 16)}... | Events: ${stats.total_events || 0}`);
|
||||
}
|
||||
|
||||
// ==== Phase 5: Persistence ====
|
||||
console.log(`\n${c.bold}=== Phase 5: Persistence ===${c.reset}\n`);
|
||||
|
||||
const { states, networkState } = network.persistAll();
|
||||
|
||||
console.log(` ${c.green}✓${c.reset} Network state persisted`);
|
||||
console.log(` Contributors: ${networkState.contributors.length}`);
|
||||
console.log(` Messages: ${networkState.messages.length}`);
|
||||
console.log(` Total patterns: ${networkState.totalPatterns}`);
|
||||
|
||||
for (const [id, state] of Object.entries(states)) {
|
||||
console.log(`\n ${c.cyan}[${id}]${c.reset} State saved:`);
|
||||
console.log(` Node ID: ${state.nodeId}`);
|
||||
console.log(` Patterns: ${state.stats.patternCount}`);
|
||||
console.log(` Ledger TX: ${state.stats.txCount}`);
|
||||
}
|
||||
|
||||
// ==== Phase 6: Verify Persistence ====
|
||||
console.log(`\n${c.bold}=== Phase 6: Verify Persistence Files ===${c.reset}\n`);
|
||||
|
||||
const files = readdirSync(storageDir);
|
||||
console.log(` Files in ${storageDir}:`);
|
||||
files.forEach(f => {
|
||||
const path = join(storageDir, f);
|
||||
const stat = existsSync(path) ? readFileSync(path).length : 0;
|
||||
console.log(` ${c.dim}•${c.reset} ${f} (${stat} bytes)`);
|
||||
});
|
||||
|
||||
// ==== Summary ====
|
||||
console.log(`
|
||||
${c.cyan}╔═══════════════════════════════════════════════════════════════╗${c.reset}
|
||||
${c.cyan}║${c.reset} ${c.bold}${c.green}All Tests Passed!${c.reset} ${c.cyan}║${c.reset}
|
||||
${c.cyan}╚═══════════════════════════════════════════════════════════════╝${c.reset}
|
||||
|
||||
${c.bold}Summary:${c.reset}
|
||||
• ${c.green}✓${c.reset} ${network.contributors.size} contributors initialized with persistent identities
|
||||
• ${c.green}✓${c.reset} ${verifications.length} cross-verifications passed
|
||||
• ${c.green}✓${c.reset} ${totalPatterns.length} patterns stored and searchable
|
||||
• ${c.green}✓${c.reset} State persisted to ${storageDir}
|
||||
• ${c.green}✓${c.reset} ${isContinuation ? 'Continued from' : 'Started'} session
|
||||
|
||||
${c.dim}Run again to test persistence restoration!${c.reset}
|
||||
`);
|
||||
|
||||
} finally {
|
||||
network.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Run
|
||||
runMultiContributorTest().catch(err => {
|
||||
console.error(`${c.red}Error: ${err.message}${c.reset}`);
|
||||
console.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue