@ruvector/edge-net 0.5.0 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -10
- package/core-invariants.js +942 -0
- package/models/adapter-hub.js +1008 -0
- package/models/adapter-security.js +792 -0
- package/models/benchmark.js +688 -0
- package/models/distribution.js +791 -0
- package/models/index.js +109 -0
- package/models/integrity.js +753 -0
- package/models/loader.js +725 -0
- package/models/microlora.js +1298 -0
- package/models/model-loader.js +922 -0
- package/models/model-optimizer.js +1245 -0
- package/models/model-registry.js +696 -0
- package/models/model-utils.js +548 -0
- package/models/models-cli.js +914 -0
- package/models/registry.json +214 -0
- package/models/training-utils.js +1418 -0
- package/models/wasm-core.js +1025 -0
- package/network-genesis.js +2847 -0
- package/onnx-worker.js +462 -8
- package/package.json +33 -3
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +610 -21
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Net Core Invariants
|
|
3
|
+
*
|
|
4
|
+
* Cogito, Creo, Codex — The system thinks collectively, creates through
|
|
5
|
+
* contribution, and codifies trust in cryptographic proof.
|
|
6
|
+
*
|
|
7
|
+
* These invariants are NOT configurable. They define what Edge-net IS.
|
|
8
|
+
*
|
|
9
|
+
* @module @ruvector/edge-net/core-invariants
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { createHash, randomBytes } from 'crypto';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// INVARIANT 1: ECONOMIC SETTLEMENT ISOLATION
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* EconomicBoundary - Enforces that plugins can NEVER execute settlement
|
|
21
|
+
*
|
|
22
|
+
* Core operations (immutable):
|
|
23
|
+
* - rUv minting
|
|
24
|
+
* - rUv burning
|
|
25
|
+
* - Credit settlement
|
|
26
|
+
* - Balance enforcement
|
|
27
|
+
* - Slashing execution
|
|
28
|
+
*
|
|
29
|
+
* Plugin operations (read-only):
|
|
30
|
+
* - Pricing suggestions
|
|
31
|
+
* - Reputation scoring
|
|
32
|
+
* - Economic modeling
|
|
33
|
+
* - Auction mechanisms
|
|
34
|
+
*/
|
|
35
|
+
export class EconomicBoundary {
|
|
36
|
+
constructor(creditSystem) {
|
|
37
|
+
this.creditSystem = creditSystem;
|
|
38
|
+
this.settlementLock = false;
|
|
39
|
+
this.coreOperationCounts = {
|
|
40
|
+
mint: 0,
|
|
41
|
+
burn: 0,
|
|
42
|
+
settle: 0,
|
|
43
|
+
slash: 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Seal the boundary - plugins get a read-only proxy
|
|
47
|
+
this._sealed = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get a read-only view for plugins
|
|
52
|
+
* Plugins can observe but NEVER modify
|
|
53
|
+
*/
|
|
54
|
+
getPluginView() {
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
// Read-only accessors
|
|
57
|
+
getBalance: (nodeId) => this.creditSystem.getBalance(nodeId),
|
|
58
|
+
getTransactionHistory: (nodeId, limit) =>
|
|
59
|
+
this.creditSystem.getTransactionHistory(nodeId, limit),
|
|
60
|
+
getSummary: () => {
|
|
61
|
+
const summary = this.creditSystem.getSummary();
|
|
62
|
+
// Remove sensitive internal state
|
|
63
|
+
delete summary.recentTransactions;
|
|
64
|
+
return Object.freeze(summary);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// Event subscription (read-only)
|
|
68
|
+
on: (event, handler) => {
|
|
69
|
+
// Only allow observation events
|
|
70
|
+
const allowedEvents = [
|
|
71
|
+
'credits-earned',
|
|
72
|
+
'credits-spent',
|
|
73
|
+
'insufficient-funds',
|
|
74
|
+
];
|
|
75
|
+
if (allowedEvents.includes(event)) {
|
|
76
|
+
this.creditSystem.on(event, (data) => {
|
|
77
|
+
// Clone data to prevent mutation
|
|
78
|
+
handler(JSON.parse(JSON.stringify(data)));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Explicit denial methods (throw if called)
|
|
84
|
+
mint: () => {
|
|
85
|
+
throw new Error('INVARIANT VIOLATION: Plugins cannot mint credits');
|
|
86
|
+
},
|
|
87
|
+
burn: () => {
|
|
88
|
+
throw new Error('INVARIANT VIOLATION: Plugins cannot burn credits');
|
|
89
|
+
},
|
|
90
|
+
settle: () => {
|
|
91
|
+
throw new Error('INVARIANT VIOLATION: Plugins cannot settle credits');
|
|
92
|
+
},
|
|
93
|
+
transfer: () => {
|
|
94
|
+
throw new Error('INVARIANT VIOLATION: Plugins cannot transfer credits');
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Core-only: Mint credits (bootstrap, rewards)
|
|
101
|
+
* @private - Only callable from core system
|
|
102
|
+
*/
|
|
103
|
+
_coreMint(nodeId, amount, reason, proofOfWork) {
|
|
104
|
+
if (!proofOfWork) {
|
|
105
|
+
throw new Error('INVARIANT: Minting requires proof of work');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.coreOperationCounts.mint++;
|
|
109
|
+
return this.creditSystem.ledger.credit(amount, JSON.stringify({
|
|
110
|
+
type: 'core_mint',
|
|
111
|
+
reason,
|
|
112
|
+
proofHash: createHash('sha256').update(JSON.stringify(proofOfWork)).digest('hex'),
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Core-only: Burn credits (slashing, expiry)
|
|
119
|
+
* @private - Only callable from core system
|
|
120
|
+
*/
|
|
121
|
+
_coreBurn(nodeId, amount, reason, evidence) {
|
|
122
|
+
this.coreOperationCounts.burn++;
|
|
123
|
+
return this.creditSystem.ledger.debit(amount, JSON.stringify({
|
|
124
|
+
type: 'core_burn',
|
|
125
|
+
reason,
|
|
126
|
+
evidenceHash: evidence ? createHash('sha256').update(JSON.stringify(evidence)).digest('hex') : null,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Core-only: Execute slashing
|
|
133
|
+
* @private - Only callable from core system
|
|
134
|
+
*/
|
|
135
|
+
_coreSlash(nodeId, amount, violation, evidence) {
|
|
136
|
+
this.coreOperationCounts.slash++;
|
|
137
|
+
|
|
138
|
+
// Record slashing event
|
|
139
|
+
const slashRecord = {
|
|
140
|
+
type: 'slash',
|
|
141
|
+
nodeId,
|
|
142
|
+
amount,
|
|
143
|
+
violation,
|
|
144
|
+
evidenceHash: createHash('sha256').update(JSON.stringify(evidence)).digest('hex'),
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Execute burn
|
|
149
|
+
this._coreBurn(nodeId, amount, `Slashed: ${violation}`, evidence);
|
|
150
|
+
|
|
151
|
+
return slashRecord;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================
|
|
156
|
+
// INVARIANT 2: IDENTITY ANTI-SYBIL MEASURES
|
|
157
|
+
// ============================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* IdentityFriction - Prevents Sybil attacks on the plugin marketplace
|
|
161
|
+
*
|
|
162
|
+
* Mechanisms:
|
|
163
|
+
* - Delayed activation (24h window)
|
|
164
|
+
* - Reputation warm-up (0.1 → 1.0 over 100 tasks)
|
|
165
|
+
* - Stake requirement for priority
|
|
166
|
+
* - Witness diversity requirement
|
|
167
|
+
*/
|
|
168
|
+
export class IdentityFriction extends EventEmitter {
|
|
169
|
+
constructor(options = {}) {
|
|
170
|
+
super();
|
|
171
|
+
|
|
172
|
+
this.config = {
|
|
173
|
+
// Delayed activation
|
|
174
|
+
activationDelayMs: options.activationDelayMs ?? 24 * 60 * 60 * 1000, // 24 hours
|
|
175
|
+
|
|
176
|
+
// Reputation warm-up
|
|
177
|
+
initialReputation: options.initialReputation ?? 0.1,
|
|
178
|
+
maxReputation: options.maxReputation ?? 1.0,
|
|
179
|
+
warmupTasks: options.warmupTasks ?? 100,
|
|
180
|
+
|
|
181
|
+
// Stake requirements
|
|
182
|
+
stakeForPriority: options.stakeForPriority ?? 10, // rUv
|
|
183
|
+
stakeSlashPercent: options.stakeSlashPercent ?? 0.5, // 50%
|
|
184
|
+
|
|
185
|
+
// Witness diversity
|
|
186
|
+
minWitnessDiversity: options.minWitnessDiversity ?? 3,
|
|
187
|
+
witnessDiversityWindowMs: options.witnessDiversityWindowMs ?? 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Identity registry
|
|
191
|
+
this.identities = new Map(); // nodeId -> IdentityRecord
|
|
192
|
+
this._activationTimers = new Map(); // nodeId -> timerId (for cleanup)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register a new identity
|
|
197
|
+
*/
|
|
198
|
+
registerIdentity(nodeId, publicKey) {
|
|
199
|
+
if (this.identities.has(nodeId)) {
|
|
200
|
+
throw new Error('Identity already registered');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const record = {
|
|
204
|
+
nodeId,
|
|
205
|
+
publicKeyHash: createHash('sha256').update(publicKey).digest('hex'),
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
activatedAt: null, // Set after delay
|
|
208
|
+
reputation: this.config.initialReputation,
|
|
209
|
+
tasksCompleted: 0,
|
|
210
|
+
stake: 0,
|
|
211
|
+
witnesses: [], // { nodeId, createdAt, attestedAt }
|
|
212
|
+
status: 'pending', // pending | active | suspended | slashed
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
this.identities.set(nodeId, record);
|
|
216
|
+
|
|
217
|
+
// Schedule activation with tracked timer
|
|
218
|
+
const timerId = setTimeout(() => {
|
|
219
|
+
this._activationTimers.delete(nodeId);
|
|
220
|
+
this._activateIdentity(nodeId);
|
|
221
|
+
}, this.config.activationDelayMs);
|
|
222
|
+
this._activationTimers.set(nodeId, timerId);
|
|
223
|
+
|
|
224
|
+
this.emit('identity:registered', { nodeId, activatesAt: Date.now() + this.config.activationDelayMs });
|
|
225
|
+
|
|
226
|
+
return record;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Activate identity after delay
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
_activateIdentity(nodeId) {
|
|
234
|
+
const record = this.identities.get(nodeId);
|
|
235
|
+
if (!record) return;
|
|
236
|
+
|
|
237
|
+
if (record.status === 'pending') {
|
|
238
|
+
record.activatedAt = Date.now();
|
|
239
|
+
record.status = 'active';
|
|
240
|
+
this.emit('identity:activated', { nodeId });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if identity can execute tasks
|
|
246
|
+
*/
|
|
247
|
+
canExecuteTasks(nodeId) {
|
|
248
|
+
const record = this.identities.get(nodeId);
|
|
249
|
+
if (!record) return { allowed: false, reason: 'Unknown identity' };
|
|
250
|
+
|
|
251
|
+
if (record.status === 'pending') {
|
|
252
|
+
const remainingMs = (record.createdAt + this.config.activationDelayMs) - Date.now();
|
|
253
|
+
return {
|
|
254
|
+
allowed: false,
|
|
255
|
+
reason: 'Pending activation',
|
|
256
|
+
remainingMs: Math.max(0, remainingMs),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (record.status === 'suspended' || record.status === 'slashed') {
|
|
261
|
+
return { allowed: false, reason: `Identity ${record.status}` };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { allowed: true, reputation: record.reputation };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Record task completion and update reputation
|
|
269
|
+
*/
|
|
270
|
+
recordTaskCompletion(nodeId, taskId, success, witnesses = []) {
|
|
271
|
+
const record = this.identities.get(nodeId);
|
|
272
|
+
if (!record) return null;
|
|
273
|
+
|
|
274
|
+
if (success) {
|
|
275
|
+
record.tasksCompleted++;
|
|
276
|
+
|
|
277
|
+
// Warm-up reputation curve
|
|
278
|
+
const progress = Math.min(record.tasksCompleted / this.config.warmupTasks, 1);
|
|
279
|
+
const reputationRange = this.config.maxReputation - this.config.initialReputation;
|
|
280
|
+
record.reputation = this.config.initialReputation + (reputationRange * progress);
|
|
281
|
+
|
|
282
|
+
// Record witnesses
|
|
283
|
+
for (const witness of witnesses) {
|
|
284
|
+
const witnessRecord = this.identities.get(witness.nodeId);
|
|
285
|
+
if (witnessRecord && this._isWitnessDiverse(record, witnessRecord)) {
|
|
286
|
+
record.witnesses.push({
|
|
287
|
+
nodeId: witness.nodeId,
|
|
288
|
+
createdAt: witnessRecord.createdAt,
|
|
289
|
+
attestedAt: Date.now(),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.emit('task:recorded', {
|
|
296
|
+
nodeId,
|
|
297
|
+
taskId,
|
|
298
|
+
success,
|
|
299
|
+
reputation: record.reputation,
|
|
300
|
+
tasksCompleted: record.tasksCompleted,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
return record;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check witness diversity (different creation times)
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
_isWitnessDiverse(identity, witness) {
|
|
311
|
+
// Witnesses must be created at different times (not Sybil cluster)
|
|
312
|
+
const timeDiff = Math.abs(identity.createdAt - witness.createdAt);
|
|
313
|
+
return timeDiff > this.config.witnessDiversityWindowMs;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Stake for priority access
|
|
318
|
+
*/
|
|
319
|
+
stake(nodeId, amount) {
|
|
320
|
+
const record = this.identities.get(nodeId);
|
|
321
|
+
if (!record) throw new Error('Unknown identity');
|
|
322
|
+
|
|
323
|
+
record.stake += amount;
|
|
324
|
+
this.emit('identity:staked', { nodeId, amount, totalStake: record.stake });
|
|
325
|
+
|
|
326
|
+
return record;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Slash stake for violations
|
|
331
|
+
*/
|
|
332
|
+
slashStake(nodeId, violation, evidence) {
|
|
333
|
+
const record = this.identities.get(nodeId);
|
|
334
|
+
if (!record) throw new Error('Unknown identity');
|
|
335
|
+
|
|
336
|
+
const slashAmount = Math.floor(record.stake * this.config.stakeSlashPercent);
|
|
337
|
+
record.stake -= slashAmount;
|
|
338
|
+
|
|
339
|
+
// Reduce reputation
|
|
340
|
+
record.reputation = Math.max(this.config.initialReputation, record.reputation * 0.5);
|
|
341
|
+
|
|
342
|
+
if (record.stake <= 0 && record.reputation <= this.config.initialReputation) {
|
|
343
|
+
record.status = 'slashed';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.emit('identity:slashed', {
|
|
347
|
+
nodeId,
|
|
348
|
+
violation,
|
|
349
|
+
slashAmount,
|
|
350
|
+
remainingStake: record.stake,
|
|
351
|
+
reputation: record.reputation,
|
|
352
|
+
status: record.status,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return { slashAmount, record };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get identity status
|
|
360
|
+
*/
|
|
361
|
+
getIdentity(nodeId) {
|
|
362
|
+
const record = this.identities.get(nodeId);
|
|
363
|
+
if (!record) return null;
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
nodeId: record.nodeId,
|
|
367
|
+
status: record.status,
|
|
368
|
+
reputation: record.reputation,
|
|
369
|
+
tasksCompleted: record.tasksCompleted,
|
|
370
|
+
stake: record.stake,
|
|
371
|
+
witnessCount: record.witnesses.length,
|
|
372
|
+
createdAt: record.createdAt,
|
|
373
|
+
activatedAt: record.activatedAt,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check if identity has priority (staked)
|
|
379
|
+
*/
|
|
380
|
+
hasPriority(nodeId) {
|
|
381
|
+
const record = this.identities.get(nodeId);
|
|
382
|
+
if (!record) return false;
|
|
383
|
+
return record.stake >= this.config.stakeForPriority;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clean up all timers and resources
|
|
388
|
+
*/
|
|
389
|
+
destroy() {
|
|
390
|
+
// Clear all activation timers
|
|
391
|
+
for (const timerId of this._activationTimers.values()) {
|
|
392
|
+
clearTimeout(timerId);
|
|
393
|
+
}
|
|
394
|
+
this._activationTimers.clear();
|
|
395
|
+
|
|
396
|
+
// Clear identity data
|
|
397
|
+
this.identities.clear();
|
|
398
|
+
|
|
399
|
+
this.removeAllListeners();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ============================================
|
|
404
|
+
// INVARIANT 3: VERIFIABLE WORK
|
|
405
|
+
// ============================================
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* WorkVerifier - Ensures "Verifiable work or no reward"
|
|
409
|
+
*
|
|
410
|
+
* All credit issuance requires cryptographic proof of work completion.
|
|
411
|
+
* Unverifiable claims are rejected, not trusted.
|
|
412
|
+
*/
|
|
413
|
+
export class WorkVerifier extends EventEmitter {
|
|
414
|
+
constructor(options = {}) {
|
|
415
|
+
super();
|
|
416
|
+
|
|
417
|
+
this.config = {
|
|
418
|
+
// Redundancy for verification
|
|
419
|
+
redundancyPercent: options.redundancyPercent ?? 0.05, // 5% of tasks
|
|
420
|
+
redundancyThreshold: options.redundancyThreshold ?? 2, // 2 matching results
|
|
421
|
+
|
|
422
|
+
// Challenger system
|
|
423
|
+
challengeWindowMs: options.challengeWindowMs ?? 60 * 60 * 1000, // 1 hour
|
|
424
|
+
challengeStake: options.challengeStake ?? 5, // rUv to challenge
|
|
425
|
+
challengeReward: options.challengeReward ?? 10, // rUv if challenge succeeds
|
|
426
|
+
|
|
427
|
+
// Result hashing
|
|
428
|
+
hashAlgorithm: 'sha256',
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Pending verifications
|
|
432
|
+
this.pendingWork = new Map(); // taskId -> WorkRecord
|
|
433
|
+
this.challenges = new Map(); // taskId -> Challenge[]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Submit work for verification
|
|
438
|
+
*/
|
|
439
|
+
submitWork(taskId, nodeId, result, executionProof) {
|
|
440
|
+
const resultHash = this._hashResult(result);
|
|
441
|
+
const proofHash = createHash(this.config.hashAlgorithm)
|
|
442
|
+
.update(JSON.stringify(executionProof))
|
|
443
|
+
.digest('hex');
|
|
444
|
+
|
|
445
|
+
const workRecord = {
|
|
446
|
+
taskId,
|
|
447
|
+
nodeId,
|
|
448
|
+
resultHash,
|
|
449
|
+
proofHash,
|
|
450
|
+
submittedAt: Date.now(),
|
|
451
|
+
challengeDeadline: Date.now() + this.config.challengeWindowMs,
|
|
452
|
+
status: 'pending', // pending | verified | challenged | rejected
|
|
453
|
+
redundantResults: [],
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
this.pendingWork.set(taskId, workRecord);
|
|
457
|
+
|
|
458
|
+
// Check if this should be redundantly executed
|
|
459
|
+
if (Math.random() < this.config.redundancyPercent) {
|
|
460
|
+
workRecord.requiresRedundancy = true;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.emit('work:submitted', { taskId, nodeId, resultHash });
|
|
464
|
+
|
|
465
|
+
return workRecord;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Submit redundant execution result
|
|
470
|
+
*/
|
|
471
|
+
submitRedundantResult(taskId, nodeId, result) {
|
|
472
|
+
const workRecord = this.pendingWork.get(taskId);
|
|
473
|
+
if (!workRecord) throw new Error('Unknown task');
|
|
474
|
+
|
|
475
|
+
const resultHash = this._hashResult(result);
|
|
476
|
+
|
|
477
|
+
workRecord.redundantResults.push({
|
|
478
|
+
nodeId,
|
|
479
|
+
resultHash,
|
|
480
|
+
submittedAt: Date.now(),
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Check for consensus
|
|
484
|
+
const matchingResults = workRecord.redundantResults.filter(
|
|
485
|
+
r => r.resultHash === workRecord.resultHash
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (matchingResults.length >= this.config.redundancyThreshold) {
|
|
489
|
+
workRecord.status = 'verified';
|
|
490
|
+
this.emit('work:verified', { taskId, method: 'redundancy' });
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return workRecord;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Challenge a work result
|
|
498
|
+
*/
|
|
499
|
+
challenge(taskId, challengerNodeId, stake, evidence) {
|
|
500
|
+
const workRecord = this.pendingWork.get(taskId);
|
|
501
|
+
if (!workRecord) throw new Error('Unknown task');
|
|
502
|
+
|
|
503
|
+
if (Date.now() > workRecord.challengeDeadline) {
|
|
504
|
+
throw new Error('Challenge window closed');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (stake < this.config.challengeStake) {
|
|
508
|
+
throw new Error(`Insufficient stake: ${stake} < ${this.config.challengeStake}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const challenge = {
|
|
512
|
+
challengerId: challengerNodeId,
|
|
513
|
+
stake,
|
|
514
|
+
evidenceHash: createHash(this.config.hashAlgorithm)
|
|
515
|
+
.update(JSON.stringify(evidence))
|
|
516
|
+
.digest('hex'),
|
|
517
|
+
submittedAt: Date.now(),
|
|
518
|
+
status: 'pending', // pending | accepted | rejected
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (!this.challenges.has(taskId)) {
|
|
522
|
+
this.challenges.set(taskId, []);
|
|
523
|
+
}
|
|
524
|
+
this.challenges.get(taskId).push(challenge);
|
|
525
|
+
|
|
526
|
+
workRecord.status = 'challenged';
|
|
527
|
+
|
|
528
|
+
this.emit('work:challenged', { taskId, challengerId: challengerNodeId });
|
|
529
|
+
|
|
530
|
+
return challenge;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Resolve a challenge (requires arbitration)
|
|
535
|
+
*/
|
|
536
|
+
resolveChallenge(taskId, challengeIndex, isValid, arbitrationProof) {
|
|
537
|
+
const workRecord = this.pendingWork.get(taskId);
|
|
538
|
+
const challenges = this.challenges.get(taskId);
|
|
539
|
+
|
|
540
|
+
if (!workRecord || !challenges || !challenges[challengeIndex]) {
|
|
541
|
+
throw new Error('Challenge not found');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const challenge = challenges[challengeIndex];
|
|
545
|
+
|
|
546
|
+
if (isValid) {
|
|
547
|
+
// Challenge succeeded - work was invalid
|
|
548
|
+
challenge.status = 'accepted';
|
|
549
|
+
workRecord.status = 'rejected';
|
|
550
|
+
|
|
551
|
+
this.emit('challenge:accepted', {
|
|
552
|
+
taskId,
|
|
553
|
+
challengerId: challenge.challengerId,
|
|
554
|
+
reward: this.config.challengeReward,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
challengerReward: this.config.challengeReward + challenge.stake,
|
|
559
|
+
workerSlash: true,
|
|
560
|
+
};
|
|
561
|
+
} else {
|
|
562
|
+
// Challenge failed - work was valid
|
|
563
|
+
challenge.status = 'rejected';
|
|
564
|
+
workRecord.status = 'verified';
|
|
565
|
+
|
|
566
|
+
this.emit('challenge:rejected', {
|
|
567
|
+
taskId,
|
|
568
|
+
challengerId: challenge.challengerId,
|
|
569
|
+
stakeLost: challenge.stake,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
challengerLoss: challenge.stake,
|
|
574
|
+
workerReward: challenge.stake * 0.5, // Half of stake goes to worker
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Finalize work after challenge window
|
|
581
|
+
*/
|
|
582
|
+
finalizeWork(taskId) {
|
|
583
|
+
const workRecord = this.pendingWork.get(taskId);
|
|
584
|
+
if (!workRecord) throw new Error('Unknown task');
|
|
585
|
+
|
|
586
|
+
if (Date.now() < workRecord.challengeDeadline) {
|
|
587
|
+
throw new Error('Challenge window still open');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (workRecord.status === 'pending') {
|
|
591
|
+
// No challenges and redundancy passed (if required)
|
|
592
|
+
if (workRecord.requiresRedundancy) {
|
|
593
|
+
const matchingResults = workRecord.redundantResults.filter(
|
|
594
|
+
r => r.resultHash === workRecord.resultHash
|
|
595
|
+
);
|
|
596
|
+
if (matchingResults.length < this.config.redundancyThreshold) {
|
|
597
|
+
workRecord.status = 'rejected';
|
|
598
|
+
this.emit('work:rejected', { taskId, reason: 'Insufficient redundancy' });
|
|
599
|
+
return workRecord;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
workRecord.status = 'verified';
|
|
604
|
+
this.emit('work:finalized', { taskId });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return workRecord;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Check if work is verified
|
|
612
|
+
*/
|
|
613
|
+
isVerified(taskId) {
|
|
614
|
+
const workRecord = this.pendingWork.get(taskId);
|
|
615
|
+
return workRecord?.status === 'verified';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Hash result deterministically
|
|
620
|
+
* @private
|
|
621
|
+
*/
|
|
622
|
+
_hashResult(result) {
|
|
623
|
+
// Canonical JSON serialization with deep key sorting
|
|
624
|
+
const sortKeys = (obj) => {
|
|
625
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
626
|
+
if (Array.isArray(obj)) return obj.map(sortKeys);
|
|
627
|
+
return Object.keys(obj).sort().reduce((acc, key) => {
|
|
628
|
+
acc[key] = sortKeys(obj[key]);
|
|
629
|
+
return acc;
|
|
630
|
+
}, {});
|
|
631
|
+
};
|
|
632
|
+
const canonical = JSON.stringify(sortKeys(result));
|
|
633
|
+
return createHash(this.config.hashAlgorithm).update(canonical).digest('hex');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Clean up old work records to prevent memory leak
|
|
638
|
+
*/
|
|
639
|
+
pruneOldRecords(maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
640
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
641
|
+
let pruned = 0;
|
|
642
|
+
|
|
643
|
+
for (const [taskId, record] of this.pendingWork) {
|
|
644
|
+
if (record.submittedAt < cutoff && record.status !== 'pending') {
|
|
645
|
+
this.pendingWork.delete(taskId);
|
|
646
|
+
this.challenges.delete(taskId);
|
|
647
|
+
pruned++;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return pruned;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ============================================
|
|
656
|
+
// INVARIANT 4: DEGRADATION OVER HALT
|
|
657
|
+
// ============================================
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* DegradationController - Ensures system never halts
|
|
661
|
+
*
|
|
662
|
+
* The system degrades gracefully under load, attack, or partial failure.
|
|
663
|
+
* Consistency is sacrificed before availability.
|
|
664
|
+
*/
|
|
665
|
+
export class DegradationController extends EventEmitter {
|
|
666
|
+
constructor(options = {}) {
|
|
667
|
+
super();
|
|
668
|
+
|
|
669
|
+
this.config = {
|
|
670
|
+
// Load thresholds
|
|
671
|
+
warningLoadPercent: options.warningLoadPercent ?? 70,
|
|
672
|
+
criticalLoadPercent: options.criticalLoadPercent ?? 90,
|
|
673
|
+
maxLoadPercent: options.maxLoadPercent ?? 100,
|
|
674
|
+
|
|
675
|
+
// Degradation levels
|
|
676
|
+
levels: ['normal', 'elevated', 'degraded', 'emergency'],
|
|
677
|
+
|
|
678
|
+
// Auto-recovery
|
|
679
|
+
recoveryCheckIntervalMs: options.recoveryCheckIntervalMs ?? 30000,
|
|
680
|
+
|
|
681
|
+
// Memory management
|
|
682
|
+
maxHistorySize: options.maxHistorySize ?? 1000,
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
this.currentLevel = 'normal';
|
|
686
|
+
// Protected metrics keys - prevents prototype pollution
|
|
687
|
+
this._allowedMetricKeys = new Set(['cpuLoad', 'memoryUsage', 'pendingTasks', 'errorRate']);
|
|
688
|
+
this.metrics = {
|
|
689
|
+
cpuLoad: 0,
|
|
690
|
+
memoryUsage: 0,
|
|
691
|
+
pendingTasks: 0,
|
|
692
|
+
errorRate: 0,
|
|
693
|
+
lastUpdated: Date.now(),
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
this.degradationHistory = [];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Update system metrics
|
|
701
|
+
* Protected against prototype pollution - only allowed keys are updated
|
|
702
|
+
*/
|
|
703
|
+
updateMetrics(metrics) {
|
|
704
|
+
// Only copy allowed metric keys to prevent prototype pollution
|
|
705
|
+
for (const key of this._allowedMetricKeys) {
|
|
706
|
+
if (Object.hasOwn(metrics, key) && typeof metrics[key] === 'number') {
|
|
707
|
+
this.metrics[key] = metrics[key];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
this.metrics.lastUpdated = Date.now();
|
|
711
|
+
|
|
712
|
+
this._evaluateDegradation();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Evaluate and apply degradation level
|
|
717
|
+
* @private
|
|
718
|
+
*/
|
|
719
|
+
_evaluateDegradation() {
|
|
720
|
+
const load = this._calculateOverallLoad();
|
|
721
|
+
const previousLevel = this.currentLevel;
|
|
722
|
+
|
|
723
|
+
if (load >= this.config.maxLoadPercent) {
|
|
724
|
+
this.currentLevel = 'emergency';
|
|
725
|
+
} else if (load >= this.config.criticalLoadPercent) {
|
|
726
|
+
this.currentLevel = 'degraded';
|
|
727
|
+
} else if (load >= this.config.warningLoadPercent) {
|
|
728
|
+
this.currentLevel = 'elevated';
|
|
729
|
+
} else {
|
|
730
|
+
this.currentLevel = 'normal';
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (this.currentLevel !== previousLevel) {
|
|
734
|
+
this.degradationHistory.push({
|
|
735
|
+
from: previousLevel,
|
|
736
|
+
to: this.currentLevel,
|
|
737
|
+
load,
|
|
738
|
+
timestamp: Date.now(),
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Prune history to prevent memory leak
|
|
742
|
+
if (this.degradationHistory.length > this.config.maxHistorySize) {
|
|
743
|
+
this.degradationHistory.splice(0, this.degradationHistory.length - this.config.maxHistorySize);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
this.emit('level:changed', {
|
|
747
|
+
from: previousLevel,
|
|
748
|
+
to: this.currentLevel,
|
|
749
|
+
load,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Calculate overall load
|
|
756
|
+
* @private
|
|
757
|
+
*/
|
|
758
|
+
_calculateOverallLoad() {
|
|
759
|
+
return Math.max(
|
|
760
|
+
this.metrics.cpuLoad || 0,
|
|
761
|
+
this.metrics.memoryUsage || 0,
|
|
762
|
+
(this.metrics.errorRate || 0) * 100
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get current degradation policy
|
|
768
|
+
*/
|
|
769
|
+
getPolicy() {
|
|
770
|
+
const policies = {
|
|
771
|
+
normal: {
|
|
772
|
+
acceptNewTasks: true,
|
|
773
|
+
pluginsEnabled: true,
|
|
774
|
+
redundancyEnabled: true,
|
|
775
|
+
maxConcurrency: Infinity,
|
|
776
|
+
taskTimeout: 30000,
|
|
777
|
+
},
|
|
778
|
+
elevated: {
|
|
779
|
+
acceptNewTasks: true,
|
|
780
|
+
pluginsEnabled: true,
|
|
781
|
+
redundancyEnabled: false, // Disable redundancy to reduce load
|
|
782
|
+
maxConcurrency: 100,
|
|
783
|
+
taskTimeout: 20000,
|
|
784
|
+
},
|
|
785
|
+
degraded: {
|
|
786
|
+
acceptNewTasks: true,
|
|
787
|
+
pluginsEnabled: false, // Disable non-core plugins
|
|
788
|
+
redundancyEnabled: false,
|
|
789
|
+
maxConcurrency: 50,
|
|
790
|
+
taskTimeout: 10000,
|
|
791
|
+
},
|
|
792
|
+
emergency: {
|
|
793
|
+
acceptNewTasks: false, // Shed load
|
|
794
|
+
pluginsEnabled: false,
|
|
795
|
+
redundancyEnabled: false,
|
|
796
|
+
maxConcurrency: 10,
|
|
797
|
+
taskTimeout: 5000,
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
level: this.currentLevel,
|
|
803
|
+
...policies[this.currentLevel],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Check if action is allowed under current policy
|
|
809
|
+
*/
|
|
810
|
+
isAllowed(action) {
|
|
811
|
+
const policy = this.getPolicy();
|
|
812
|
+
|
|
813
|
+
switch (action) {
|
|
814
|
+
case 'accept_task':
|
|
815
|
+
return policy.acceptNewTasks;
|
|
816
|
+
case 'load_plugin':
|
|
817
|
+
return policy.pluginsEnabled;
|
|
818
|
+
case 'redundant_execution':
|
|
819
|
+
return policy.redundancyEnabled;
|
|
820
|
+
default:
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Get current status
|
|
827
|
+
*/
|
|
828
|
+
getStatus() {
|
|
829
|
+
return {
|
|
830
|
+
level: this.currentLevel,
|
|
831
|
+
metrics: { ...this.metrics },
|
|
832
|
+
policy: this.getPolicy(),
|
|
833
|
+
history: this.degradationHistory.slice(-10),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ============================================
|
|
839
|
+
// CORE SYSTEM ORCHESTRATOR
|
|
840
|
+
// ============================================
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* CoreInvariants - Orchestrates all invariant enforcement
|
|
844
|
+
*
|
|
845
|
+
* Cogito, Creo, Codex
|
|
846
|
+
*/
|
|
847
|
+
export class CoreInvariants extends EventEmitter {
|
|
848
|
+
constructor(creditSystem, options = {}) {
|
|
849
|
+
super();
|
|
850
|
+
|
|
851
|
+
// Initialize all invariant enforcers
|
|
852
|
+
this.economicBoundary = new EconomicBoundary(creditSystem);
|
|
853
|
+
this.identityFriction = new IdentityFriction(options.identity);
|
|
854
|
+
this.workVerifier = new WorkVerifier(options.verification);
|
|
855
|
+
this.degradationController = new DegradationController(options.degradation);
|
|
856
|
+
|
|
857
|
+
// Cross-wire events
|
|
858
|
+
this._wireEvents();
|
|
859
|
+
|
|
860
|
+
console.log('[CoreInvariants] Cogito, Creo, Codex — Invariants initialized');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Wire cross-component events
|
|
865
|
+
* @private
|
|
866
|
+
*/
|
|
867
|
+
_wireEvents() {
|
|
868
|
+
// Slash identity when work is rejected
|
|
869
|
+
this.workVerifier.on('work:rejected', ({ taskId }) => {
|
|
870
|
+
const work = this.workVerifier.pendingWork.get(taskId);
|
|
871
|
+
if (work) {
|
|
872
|
+
this.identityFriction.slashStake(
|
|
873
|
+
work.nodeId,
|
|
874
|
+
'work_rejected',
|
|
875
|
+
{ taskId }
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// Emit unified events
|
|
881
|
+
this.identityFriction.on('identity:slashed', (data) => {
|
|
882
|
+
this.emit('invariant:slashing', data);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
this.degradationController.on('level:changed', (data) => {
|
|
886
|
+
this.emit('invariant:degradation', data);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Get plugin-safe view of economic system
|
|
892
|
+
*/
|
|
893
|
+
getPluginEconomicView() {
|
|
894
|
+
return this.economicBoundary.getPluginView();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Register new identity with friction
|
|
899
|
+
*/
|
|
900
|
+
registerIdentity(nodeId, publicKey) {
|
|
901
|
+
return this.identityFriction.registerIdentity(nodeId, publicKey);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Submit and verify work
|
|
906
|
+
*/
|
|
907
|
+
submitWork(taskId, nodeId, result, proof) {
|
|
908
|
+
// Check identity can execute
|
|
909
|
+
const canExecute = this.identityFriction.canExecuteTasks(nodeId);
|
|
910
|
+
if (!canExecute.allowed) {
|
|
911
|
+
throw new Error(`Identity cannot execute: ${canExecute.reason}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Check degradation allows
|
|
915
|
+
if (!this.degradationController.isAllowed('accept_task')) {
|
|
916
|
+
throw new Error('System in emergency mode, shedding load');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return this.workVerifier.submitWork(taskId, nodeId, result, proof);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Update system load metrics
|
|
924
|
+
*/
|
|
925
|
+
updateMetrics(metrics) {
|
|
926
|
+
this.degradationController.updateMetrics(metrics);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Get comprehensive status
|
|
931
|
+
*/
|
|
932
|
+
getStatus() {
|
|
933
|
+
return {
|
|
934
|
+
degradation: this.degradationController.getStatus(),
|
|
935
|
+
pendingVerifications: this.workVerifier.pendingWork.size,
|
|
936
|
+
activeChallenges: this.workVerifier.challenges.size,
|
|
937
|
+
identityCount: this.identityFriction.identities.size,
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export default CoreInvariants;
|