From bd67b26e11ded1e509e51d9d51e4e69cb5296d60 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 14:26:43 +0000 Subject: [PATCH] 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 --- examples/edge-net/pkg/join.js | 461 +++++++++++++++- .../edge-net/pkg/multi-contributor-test.js | 500 ++++++++++++++++++ 2 files changed, 949 insertions(+), 12 deletions(-) create mode 100644 examples/edge-net/pkg/multi-contributor-test.js diff --git a/examples/edge-net/pkg/join.js b/examples/edge-net/pkg/join.js index f57ae673..f203c985 100644 --- a/examples/edge-net/pkg/join.js +++ b/examples/edge-net/pkg/join.js @@ -13,7 +13,7 @@ * npx @ruvector/edge-net join --import # 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 ')} Import identity from encrypted backup ${c('yellow', '--password ')} 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); diff --git a/examples/edge-net/pkg/multi-contributor-test.js b/examples/edge-net/pkg/multi-contributor-test.js new file mode 100644 index 00000000..b8aafbcb --- /dev/null +++ b/examples/edge-net/pkg/multi-contributor-test.js @@ -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); +});