@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
|
@@ -19,6 +19,14 @@ import {
|
|
|
19
19
|
generatePluginTemplate,
|
|
20
20
|
} from '../plugins/index.js';
|
|
21
21
|
|
|
22
|
+
import { PluginFailureContract } from '../plugins/plugin-loader.js';
|
|
23
|
+
import CoreInvariants, {
|
|
24
|
+
EconomicBoundary,
|
|
25
|
+
IdentityFriction,
|
|
26
|
+
WorkVerifier,
|
|
27
|
+
DegradationController,
|
|
28
|
+
} from '../core-invariants.js';
|
|
29
|
+
|
|
22
30
|
import { CompressionPlugin } from '../plugins/implementations/compression.js';
|
|
23
31
|
import { E2EEncryptionPlugin } from '../plugins/implementations/e2e-encryption.js';
|
|
24
32
|
import { FederatedLearningPlugin } from '../plugins/implementations/federated-learning.js';
|
|
@@ -364,6 +372,161 @@ test('Swarm intelligence plugin works', async () => {
|
|
|
364
372
|
assert(result.iterations === 50, 'Should run 50 iterations');
|
|
365
373
|
});
|
|
366
374
|
|
|
375
|
+
// --- Invariant Enforcement Tests ---
|
|
376
|
+
console.log('\n--- Core Invariants (Cogito, Creo, Codex) ---\n');
|
|
377
|
+
|
|
378
|
+
test('PluginFailureContract enforces circuit breaker', async () => {
|
|
379
|
+
const contract = new PluginFailureContract({
|
|
380
|
+
maxRetries: 3,
|
|
381
|
+
quarantineDurationMs: 100, // Short for testing
|
|
382
|
+
executionTimeoutMs: 50,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Record 3 failures to trip circuit breaker
|
|
386
|
+
contract.recordFailure('test-plugin', new Error('Failure 1'));
|
|
387
|
+
contract.recordFailure('test-plugin', new Error('Failure 2'));
|
|
388
|
+
contract.recordFailure('test-plugin', new Error('Failure 3'));
|
|
389
|
+
|
|
390
|
+
// Plugin should now be blocked
|
|
391
|
+
const canExec = contract.canExecute('test-plugin');
|
|
392
|
+
assert(!canExec.allowed, 'Plugin should be blocked after 3 failures');
|
|
393
|
+
assert(canExec.reason.includes('quarantine'), 'Should mention quarantine');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('PluginFailureContract provides health status', () => {
|
|
397
|
+
const contract = new PluginFailureContract();
|
|
398
|
+
|
|
399
|
+
contract.recordFailure('healthy-plugin', new Error('Single failure'));
|
|
400
|
+
|
|
401
|
+
const health = contract.getHealth('healthy-plugin');
|
|
402
|
+
assert(health.healthy, 'Plugin with 1 failure should still be healthy');
|
|
403
|
+
assertEqual(health.failureCount, 1, 'Should track 1 failure');
|
|
404
|
+
|
|
405
|
+
const summary = contract.getSummary();
|
|
406
|
+
assertEqual(summary.totalPlugins, 1, 'Should track 1 plugin');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('Economic boundary prevents plugin credit modification', () => {
|
|
410
|
+
// Create mock credit system
|
|
411
|
+
const mockCreditSystem = {
|
|
412
|
+
getBalance: (nodeId) => 100,
|
|
413
|
+
getTransactionHistory: () => [],
|
|
414
|
+
getSummary: () => ({ balance: 100 }),
|
|
415
|
+
ledger: { credit: () => {}, debit: () => {} },
|
|
416
|
+
on: () => {},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const boundary = new EconomicBoundary(mockCreditSystem);
|
|
420
|
+
const pluginView = boundary.getPluginView();
|
|
421
|
+
|
|
422
|
+
// Read operations should work
|
|
423
|
+
assertEqual(pluginView.getBalance('node-1'), 100, 'Should read balance');
|
|
424
|
+
|
|
425
|
+
// Write operations should throw
|
|
426
|
+
let mintThrew = false;
|
|
427
|
+
try { pluginView.mint(); } catch (e) {
|
|
428
|
+
mintThrew = e.message.includes('INVARIANT VIOLATION');
|
|
429
|
+
}
|
|
430
|
+
assert(mintThrew, 'mint() should throw invariant violation');
|
|
431
|
+
|
|
432
|
+
let burnThrew = false;
|
|
433
|
+
try { pluginView.burn(); } catch (e) {
|
|
434
|
+
burnThrew = e.message.includes('INVARIANT VIOLATION');
|
|
435
|
+
}
|
|
436
|
+
assert(burnThrew, 'burn() should throw invariant violation');
|
|
437
|
+
|
|
438
|
+
let settleThrew = false;
|
|
439
|
+
try { pluginView.settle(); } catch (e) {
|
|
440
|
+
settleThrew = e.message.includes('INVARIANT VIOLATION');
|
|
441
|
+
}
|
|
442
|
+
assert(settleThrew, 'settle() should throw invariant violation');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('Identity friction enforces activation delay', () => {
|
|
446
|
+
const friction = new IdentityFriction({
|
|
447
|
+
activationDelayMs: 100, // Short for testing
|
|
448
|
+
warmupTasks: 10,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Register identity
|
|
452
|
+
friction.registerIdentity('new-node', 'test-public-key');
|
|
453
|
+
|
|
454
|
+
// Should not be able to execute immediately
|
|
455
|
+
const canExec = friction.canExecuteTasks('new-node');
|
|
456
|
+
assert(!canExec.allowed, 'New identity should not execute immediately');
|
|
457
|
+
assert(canExec.reason === 'Pending activation', 'Should be pending');
|
|
458
|
+
assert(canExec.remainingMs > 0, 'Should have remaining time');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('Work verifier tracks submitted work', () => {
|
|
462
|
+
const verifier = new WorkVerifier();
|
|
463
|
+
|
|
464
|
+
const work = verifier.submitWork('task-1', 'node-1', { result: 'done' }, { proof: 'proof' });
|
|
465
|
+
|
|
466
|
+
assert(work.taskId === 'task-1', 'Should track task ID');
|
|
467
|
+
assert(work.status === 'pending', 'Should start pending');
|
|
468
|
+
assert(work.resultHash, 'Should hash result');
|
|
469
|
+
assert(work.challengeDeadline > Date.now(), 'Should have challenge window');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('Degradation controller changes policy under load', () => {
|
|
473
|
+
const controller = new DegradationController({
|
|
474
|
+
warningLoadPercent: 70,
|
|
475
|
+
criticalLoadPercent: 90,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Normal state
|
|
479
|
+
let policy = controller.getPolicy();
|
|
480
|
+
assertEqual(policy.level, 'normal', 'Should start normal');
|
|
481
|
+
assert(policy.acceptNewTasks, 'Should accept tasks');
|
|
482
|
+
assert(policy.pluginsEnabled, 'Plugins should be enabled');
|
|
483
|
+
|
|
484
|
+
// Update to high load
|
|
485
|
+
controller.updateMetrics({ cpuLoad: 95 });
|
|
486
|
+
|
|
487
|
+
policy = controller.getPolicy();
|
|
488
|
+
assertEqual(policy.level, 'degraded', 'Should be degraded at 95% load');
|
|
489
|
+
assert(!policy.pluginsEnabled, 'Plugins should be disabled under load');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('Plugin loader provides economic boundary to sandbox', () => {
|
|
493
|
+
const loader = new PluginLoader();
|
|
494
|
+
const catalog = loader.getCatalog();
|
|
495
|
+
|
|
496
|
+
// Loader should have mock economic view
|
|
497
|
+
assert(catalog.length > 0, 'Should have plugins');
|
|
498
|
+
|
|
499
|
+
// Get stats with health
|
|
500
|
+
const stats = loader.getStats();
|
|
501
|
+
assert(stats.health, 'Stats should include health');
|
|
502
|
+
assertEqual(stats.health.totalPlugins, 0, 'No plugins tracked yet');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
testAsync('Plugin loader isolates failures', async () => {
|
|
506
|
+
const loader = new PluginLoader({
|
|
507
|
+
maxRetries: 2,
|
|
508
|
+
executionTimeoutMs: 50,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Load a plugin
|
|
512
|
+
await loader.load('compression');
|
|
513
|
+
const plugin = loader.get('compression');
|
|
514
|
+
assert(plugin, 'Plugin should load');
|
|
515
|
+
|
|
516
|
+
// Check health before failures
|
|
517
|
+
let health = loader.getHealth('compression');
|
|
518
|
+
assert(health.healthy, 'Should be healthy initially');
|
|
519
|
+
|
|
520
|
+
// Record failures to trigger circuit breaker
|
|
521
|
+
loader.failureContract.recordFailure('compression', new Error('Test failure 1'));
|
|
522
|
+
loader.failureContract.recordFailure('compression', new Error('Test failure 2'));
|
|
523
|
+
|
|
524
|
+
// Check health after failures
|
|
525
|
+
health = loader.getHealth('compression');
|
|
526
|
+
assert(!health.healthy, 'Should be unhealthy after failures');
|
|
527
|
+
assert(health.quarantine, 'Should be quarantined');
|
|
528
|
+
});
|
|
529
|
+
|
|
367
530
|
// --- Summary ---
|
|
368
531
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
369
532
|
console.log('║ TEST SUMMARY ║');
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM Core Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates security fixes and core functionality:
|
|
5
|
+
* - Cryptographic randomness (no Math.random)
|
|
6
|
+
* - Ed25519 fail-closed behavior
|
|
7
|
+
* - Memory bounds
|
|
8
|
+
* - Genesis signing and verification
|
|
9
|
+
* - Lineage verification
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, test, expect, beforeAll } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
detectPlatform,
|
|
15
|
+
getPlatformCapabilities,
|
|
16
|
+
WasmCrypto,
|
|
17
|
+
WasmGenesis,
|
|
18
|
+
WasmInference,
|
|
19
|
+
} from '../models/wasm-core.js';
|
|
20
|
+
|
|
21
|
+
describe('Platform Detection', () => {
|
|
22
|
+
test('detects Node.js platform', () => {
|
|
23
|
+
const platform = detectPlatform();
|
|
24
|
+
expect(platform).toBe('node');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('returns platform capabilities', () => {
|
|
28
|
+
const caps = getPlatformCapabilities();
|
|
29
|
+
expect(caps).toHaveProperty('platform');
|
|
30
|
+
expect(caps).toHaveProperty('hasWebAssembly');
|
|
31
|
+
expect(caps).toHaveProperty('hasWebCrypto');
|
|
32
|
+
expect(caps).toHaveProperty('maxMemory');
|
|
33
|
+
expect(typeof caps.maxMemory).toBe('number');
|
|
34
|
+
expect(caps.maxMemory).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('WasmCrypto', () => {
|
|
39
|
+
let crypto;
|
|
40
|
+
|
|
41
|
+
beforeAll(async () => {
|
|
42
|
+
crypto = new WasmCrypto();
|
|
43
|
+
await crypto.init();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('computes SHA256 hash', async () => {
|
|
47
|
+
const hash = await crypto.sha256('hello world');
|
|
48
|
+
expect(hash).toBeInstanceOf(Uint8Array);
|
|
49
|
+
expect(hash.length).toBe(32);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('computes SHA256 hex string', async () => {
|
|
53
|
+
const hashHex = await crypto.sha256Hex('hello world');
|
|
54
|
+
expect(typeof hashHex).toBe('string');
|
|
55
|
+
expect(hashHex.length).toBe(64);
|
|
56
|
+
// Known SHA256 of "hello world"
|
|
57
|
+
expect(hashHex).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('canonicalizes objects deterministically', () => {
|
|
61
|
+
const obj1 = { b: 2, a: 1 };
|
|
62
|
+
const obj2 = { a: 1, b: 2 };
|
|
63
|
+
const canon1 = crypto.canonicalize(obj1);
|
|
64
|
+
const canon2 = crypto.canonicalize(obj2);
|
|
65
|
+
expect(canon1).toBe(canon2);
|
|
66
|
+
expect(canon1).toBe('{"a":1,"b":2}');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('canonicalizes nested objects', () => {
|
|
70
|
+
const obj = { z: { b: 2, a: 1 }, y: [3, 2, 1] };
|
|
71
|
+
const canon = crypto.canonicalize(obj);
|
|
72
|
+
expect(canon).toBe('{"y":[3,2,1],"z":{"a":1,"b":2}}');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('rejects Infinity and NaN', () => {
|
|
76
|
+
expect(() => crypto.canonicalize({ x: Infinity }))
|
|
77
|
+
.toThrow('Cannot canonicalize Infinity/NaN');
|
|
78
|
+
expect(() => crypto.canonicalize({ x: NaN }))
|
|
79
|
+
.toThrow('Cannot canonicalize Infinity/NaN');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('computes hash of canonical object', async () => {
|
|
83
|
+
const hash = await crypto.hashCanonical({ test: 'value' });
|
|
84
|
+
expect(typeof hash).toBe('string');
|
|
85
|
+
expect(hash.length).toBe(64);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('merkle root of single hash returns the hash', async () => {
|
|
89
|
+
const hash = await crypto.sha256('test');
|
|
90
|
+
const root = await crypto.merkleRoot([hash]);
|
|
91
|
+
// Single element should return itself (or same value)
|
|
92
|
+
expect(root).toBeInstanceOf(Uint8Array);
|
|
93
|
+
expect(root.length).toBe(32);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('WasmGenesis Security', () => {
|
|
98
|
+
let genesis;
|
|
99
|
+
|
|
100
|
+
beforeAll(async () => {
|
|
101
|
+
genesis = new WasmGenesis({
|
|
102
|
+
networkName: 'test-net',
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
});
|
|
105
|
+
await genesis.init();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('generates cryptographically random network IDs', async () => {
|
|
109
|
+
const ids = new Set();
|
|
110
|
+
for (let i = 0; i < 10; i++) {
|
|
111
|
+
const id = await genesis._generateNetworkId(Date.now());
|
|
112
|
+
expect(id).toMatch(/^net_[a-f0-9]{16}$/);
|
|
113
|
+
ids.add(id);
|
|
114
|
+
}
|
|
115
|
+
// All IDs should be unique
|
|
116
|
+
expect(ids.size).toBe(10);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('births network with Ed25519 keypair', async () => {
|
|
120
|
+
const result = await genesis.birthNetwork({
|
|
121
|
+
traits: { test: true },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result).toHaveProperty('networkId');
|
|
125
|
+
expect(result).toHaveProperty('manifest');
|
|
126
|
+
expect(result).toHaveProperty('genesisHash');
|
|
127
|
+
expect(result).toHaveProperty('signature');
|
|
128
|
+
expect(result).toHaveProperty('publicKey');
|
|
129
|
+
|
|
130
|
+
// Verify signature format (128 hex chars = 64 bytes)
|
|
131
|
+
expect(result.signature.length).toBe(128);
|
|
132
|
+
// Verify public key format (64 hex chars = 32 bytes)
|
|
133
|
+
expect(result.publicKey.length).toBe(64);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('manifest includes cryptographic signature', async () => {
|
|
137
|
+
const result = await genesis.birthNetwork();
|
|
138
|
+
const manifest = result.manifest;
|
|
139
|
+
|
|
140
|
+
expect(manifest.integrity).toHaveProperty('signature');
|
|
141
|
+
expect(manifest.integrity).toHaveProperty('signatureAlgorithm', 'Ed25519');
|
|
142
|
+
expect(manifest.integrity).toHaveProperty('genesisHash');
|
|
143
|
+
expect(manifest.genesis).toHaveProperty('publicKey');
|
|
144
|
+
expect(manifest.genesis).toHaveProperty('keyAlgorithm', 'Ed25519');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('verifies valid genesis signature', async () => {
|
|
148
|
+
const result = await genesis.birthNetwork();
|
|
149
|
+
const verification = await genesis.verifyGenesis(result.manifest);
|
|
150
|
+
|
|
151
|
+
expect(verification.valid).toBe(true);
|
|
152
|
+
expect(verification.genesisHash).toBe(result.genesisHash);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('rejects tampered genesis', async () => {
|
|
156
|
+
const result = await genesis.birthNetwork();
|
|
157
|
+
const manifest = JSON.parse(JSON.stringify(result.manifest));
|
|
158
|
+
|
|
159
|
+
// Tamper with the genesis
|
|
160
|
+
manifest.genesis.traits.hacked = true;
|
|
161
|
+
|
|
162
|
+
const verification = await genesis.verifyGenesis(manifest);
|
|
163
|
+
expect(verification.valid).toBe(false);
|
|
164
|
+
expect(verification.error).toMatch(/hash mismatch/i);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('rejects missing signature', async () => {
|
|
168
|
+
const result = await genesis.birthNetwork();
|
|
169
|
+
const manifest = JSON.parse(JSON.stringify(result.manifest));
|
|
170
|
+
|
|
171
|
+
// Remove signature
|
|
172
|
+
delete manifest.integrity.signature;
|
|
173
|
+
|
|
174
|
+
const verification = await genesis.verifyGenesis(manifest);
|
|
175
|
+
expect(verification.valid).toBe(false);
|
|
176
|
+
expect(verification.error).toMatch(/Missing.*signature/i);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('WasmGenesis Lineage', () => {
|
|
181
|
+
let genesis;
|
|
182
|
+
|
|
183
|
+
beforeAll(async () => {
|
|
184
|
+
genesis = new WasmGenesis({ networkName: 'lineage-test' });
|
|
185
|
+
await genesis.init();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('verifies root network (no parent)', async () => {
|
|
189
|
+
const result = await genesis.birthNetwork();
|
|
190
|
+
const verification = await genesis.verifyLineage(result.manifest);
|
|
191
|
+
|
|
192
|
+
expect(verification.valid).toBe(true);
|
|
193
|
+
expect(verification.isRoot).toBe(true);
|
|
194
|
+
expect(verification.genesisVerified).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('reproduces with lineage tracking', async () => {
|
|
198
|
+
const parent = await genesis.birthNetwork({
|
|
199
|
+
traits: { generation: 0, fitness: 1.0 },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const child = await genesis.reproduce(parent.manifest, {
|
|
203
|
+
mutationRate: 0.1,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(child.manifest.lineage).toBeDefined();
|
|
207
|
+
expect(child.manifest.lineage.parentId).toBe(parent.manifest.genesis.networkId);
|
|
208
|
+
expect(child.manifest.lineage.generation).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('verifies valid lineage chain', async () => {
|
|
212
|
+
const parent = await genesis.birthNetwork();
|
|
213
|
+
const child = await genesis.reproduce(parent.manifest);
|
|
214
|
+
|
|
215
|
+
const verification = await genesis.verifyLineage(child.manifest, parent.manifest);
|
|
216
|
+
|
|
217
|
+
expect(verification.valid).toBe(true);
|
|
218
|
+
expect(verification.genesisVerified).toBe(true);
|
|
219
|
+
expect(verification.parentVerified).toBe(true);
|
|
220
|
+
expect(verification.parentId).toBe(parent.manifest.genesis.networkId);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('rejects mismatched parent ID', async () => {
|
|
224
|
+
const parent = await genesis.birthNetwork();
|
|
225
|
+
const fakeParent = await genesis.birthNetwork();
|
|
226
|
+
const child = await genesis.reproduce(parent.manifest);
|
|
227
|
+
|
|
228
|
+
// Try to verify with wrong parent
|
|
229
|
+
const verification = await genesis.verifyLineage(child.manifest, fakeParent.manifest);
|
|
230
|
+
|
|
231
|
+
expect(verification.valid).toBe(false);
|
|
232
|
+
expect(verification.error).toMatch(/Parent ID mismatch/i);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('rejects broken generation sequence', async () => {
|
|
236
|
+
const parent = await genesis.birthNetwork();
|
|
237
|
+
const child = await genesis.reproduce(parent.manifest);
|
|
238
|
+
|
|
239
|
+
// Tamper with generation
|
|
240
|
+
const tamperedManifest = JSON.parse(JSON.stringify(child.manifest));
|
|
241
|
+
tamperedManifest.lineage.generation = 5;
|
|
242
|
+
|
|
243
|
+
// Re-sign would be needed for real verification
|
|
244
|
+
// But the generation check should fail first
|
|
245
|
+
const verification = await genesis.verifyLineage(tamperedManifest, parent.manifest);
|
|
246
|
+
|
|
247
|
+
// Should fail on genesis hash mismatch since we tampered
|
|
248
|
+
expect(verification.valid).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('WasmInference', () => {
|
|
253
|
+
let inference;
|
|
254
|
+
|
|
255
|
+
beforeAll(async () => {
|
|
256
|
+
inference = new WasmInference();
|
|
257
|
+
await inference.init();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('initializes with platform capabilities', async () => {
|
|
261
|
+
expect(inference.ready).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('loads model with correct hash path', async () => {
|
|
265
|
+
const modelData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
266
|
+
const manifest = {
|
|
267
|
+
artifacts: {
|
|
268
|
+
model: {
|
|
269
|
+
sha256: await inference.crypto.sha256Hex(modelData),
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
await inference.loadModel(modelData, manifest);
|
|
275
|
+
expect(inference.model).toBeDefined();
|
|
276
|
+
expect(inference.model.manifest).toBe(manifest);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('rejects model with hash mismatch', async () => {
|
|
280
|
+
const modelData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
281
|
+
const manifest = {
|
|
282
|
+
artifacts: {
|
|
283
|
+
model: {
|
|
284
|
+
sha256: 'wrong_hash_value_here',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await expect(inference.loadModel(modelData, manifest))
|
|
290
|
+
.rejects.toThrow(/hash mismatch/i);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('supports legacy artifact format', async () => {
|
|
294
|
+
const modelData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
295
|
+
const hash = await inference.crypto.sha256Hex(modelData);
|
|
296
|
+
const manifest = {
|
|
297
|
+
artifacts: [{ sha256: hash }],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
await inference.loadModel(modelData, manifest);
|
|
301
|
+
expect(inference.model).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('Security: No Math.random', () => {
|
|
306
|
+
test('source code does not use Math.random for security', async () => {
|
|
307
|
+
const fs = await import('fs/promises');
|
|
308
|
+
const source = await fs.readFile(
|
|
309
|
+
new URL('../models/wasm-core.js', import.meta.url),
|
|
310
|
+
'utf-8'
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Find all Math.random occurrences
|
|
314
|
+
const mathRandomUsage = source.match(/Math\.random\(\)/g) || [];
|
|
315
|
+
|
|
316
|
+
// Math.random should NOT be used for security-critical operations
|
|
317
|
+
// Check that any usage is clearly documented as non-security
|
|
318
|
+
if (mathRandomUsage.length > 0) {
|
|
319
|
+
// Verify they're only in non-security contexts (placeholder code)
|
|
320
|
+
const lines = source.split('\n');
|
|
321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
322
|
+
if (lines[i].includes('Math.random()')) {
|
|
323
|
+
// Check if this line is commented or in placeholder section
|
|
324
|
+
const context = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
|
|
325
|
+
expect(context).not.toMatch(/_generateNetworkId/);
|
|
326
|
+
expect(context).not.toMatch(/sign/i);
|
|
327
|
+
expect(context).not.toMatch(/key/i);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Security: Fail Closed', () => {
|
|
335
|
+
test('Ed25519 fallback throws instead of returning zeros', async () => {
|
|
336
|
+
// The JS fallback should throw when Ed25519 is unavailable
|
|
337
|
+
// We can't easily test this without mocking crypto.subtle
|
|
338
|
+
// but we can verify the code pattern exists
|
|
339
|
+
const fs = await import('fs/promises');
|
|
340
|
+
const source = await fs.readFile(
|
|
341
|
+
new URL('../models/wasm-core.js', import.meta.url),
|
|
342
|
+
'utf-8'
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Verify fail-closed pattern exists
|
|
346
|
+
expect(source).toContain('FAIL CLOSED');
|
|
347
|
+
expect(source).toContain('throw new Error');
|
|
348
|
+
expect(source).not.toContain('returning mock signature');
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('Memory Bounds', () => {
|
|
353
|
+
test('WASM memory is reasonably bounded', async () => {
|
|
354
|
+
const fs = await import('fs/promises');
|
|
355
|
+
const source = await fs.readFile(
|
|
356
|
+
new URL('../models/wasm-core.js', import.meta.url),
|
|
357
|
+
'utf-8'
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Check that memory max is not 65536 (4GB)
|
|
361
|
+
const memoryMatch = source.match(/maximum:\s*(\d+)/);
|
|
362
|
+
if (memoryMatch) {
|
|
363
|
+
const maxPages = parseInt(memoryMatch[1], 10);
|
|
364
|
+
// Should be <= 1024 pages (64MB) for edge platforms
|
|
365
|
+
expect(maxPages).toBeLessThanOrEqual(1024);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|