yakmesh 2.8.2 → 3.0.0
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/CHANGELOG.md +637 -0
- package/CONTRIBUTING.md +42 -0
- package/Caddyfile +77 -0
- package/README.md +119 -29
- package/adapters/adapter-mlv-bible/README.md +124 -0
- package/adapters/adapter-mlv-bible/index.js +400 -0
- package/adapters/chat-mod-adapter.js +532 -0
- package/adapters/content-adapter.js +273 -0
- package/content/api.js +50 -41
- package/content/index.js +2 -2
- package/content/store.js +355 -173
- package/dashboard/index.html +19 -3
- package/database/replication.js +117 -37
- package/docs/CRYPTO-AGILITY.md +204 -0
- package/docs/MTLS-RESEARCH.md +367 -0
- package/docs/NAMCHE-SPEC.md +681 -0
- package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
- package/docs/PRECISION-DISCLOSURE.md +96 -0
- package/docs/README.md +76 -0
- package/docs/ROADMAP-2.4.0.md +447 -0
- package/docs/ROADMAP-2.5.0.md +244 -0
- package/docs/SECURITY-AUDIT-REPORT.md +306 -0
- package/docs/SST-INTEGRATION.md +712 -0
- package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
- package/docs/TERNARY-AUDIT-REPORT.md +247 -0
- package/docs/TME-FAQ.md +221 -0
- package/docs/WHITEPAPER.md +623 -0
- package/docs/adapters.html +1001 -0
- package/docs/advanced-systems.html +1045 -0
- package/docs/annex.html +1046 -0
- package/docs/api.html +970 -0
- package/docs/business/response-templates.md +160 -0
- package/docs/c2c.html +1225 -0
- package/docs/cli.html +1332 -0
- package/docs/configuration.html +1248 -0
- package/docs/darshan.html +1085 -0
- package/docs/dharma.html +966 -0
- package/docs/docs-bundle.html +1075 -0
- package/docs/docs.css +3120 -0
- package/docs/docs.js +556 -0
- package/docs/doko.html +969 -0
- package/docs/geo-proof.html +858 -0
- package/docs/getting-started.html +840 -0
- package/docs/gumba-tutorial.html +1144 -0
- package/docs/gumba.html +1098 -0
- package/docs/index.html +914 -0
- package/docs/jhilke.html +1312 -0
- package/docs/karma.html +1100 -0
- package/docs/katha.html +1037 -0
- package/docs/lama.html +978 -0
- package/docs/mandala.html +1067 -0
- package/docs/mani.html +964 -0
- package/docs/mantra.html +967 -0
- package/docs/mesh.html +1409 -0
- package/docs/nakpak.html +869 -0
- package/docs/namche.html +928 -0
- package/docs/nav-order.json +53 -0
- package/docs/prahari.html +1043 -0
- package/docs/prism-bash.min.js +1 -0
- package/docs/prism-javascript.min.js +1 -0
- package/docs/prism-json.min.js +1 -0
- package/docs/prism-tomorrow.min.css +1 -0
- package/docs/prism.min.js +1 -0
- package/docs/privacy.html +699 -0
- package/docs/quick-reference.html +1181 -0
- package/docs/sakshi.html +1402 -0
- package/docs/sandboxing.md +386 -0
- package/docs/seva.html +911 -0
- package/docs/sherpa.html +871 -0
- package/docs/studio.html +860 -0
- package/docs/stupa.html +995 -0
- package/docs/tailwind.min.css +2 -0
- package/docs/tattva.html +1332 -0
- package/docs/terms.html +686 -0
- package/docs/time-server-deployment.md +166 -0
- package/docs/time-sources.html +1392 -0
- package/docs/tivra.html +1127 -0
- package/docs/trademark-policy.html +686 -0
- package/docs/tribhuj.html +1183 -0
- package/docs/trust-security.html +1029 -0
- package/docs/tutorials/backup-recovery.html +654 -0
- package/docs/tutorials/dashboard.html +604 -0
- package/docs/tutorials/domain-setup.html +605 -0
- package/docs/tutorials/host-website.html +456 -0
- package/docs/tutorials/mesh-network.html +505 -0
- package/docs/tutorials/mobile-access.html +445 -0
- package/docs/tutorials/privacy.html +467 -0
- package/docs/tutorials/raspberry-pi.html +600 -0
- package/docs/tutorials/security-basics.html +539 -0
- package/docs/tutorials/share-files.html +431 -0
- package/docs/tutorials/troubleshooting.html +637 -0
- package/docs/tutorials/trust-karma.html +419 -0
- package/docs/tutorials/yak-protocol.html +456 -0
- package/docs/tutorials.html +1034 -0
- package/docs/vani.html +1270 -0
- package/docs/webserver.html +809 -0
- package/docs/yak-protocol.html +940 -0
- package/docs/yak-timeserver-design.md +475 -0
- package/docs/yakapp.html +1015 -0
- package/docs/ypc27.html +1069 -0
- package/docs/yurt.html +1344 -0
- package/embedded-docs/bundle.js +334 -74
- package/gossip/protocol.js +247 -27
- package/identity/key-resolver.js +262 -0
- package/identity/machine-seed.js +632 -0
- package/identity/node-key.js +669 -368
- package/identity/tribhuj-ratchet.js +506 -0
- package/knowledge-base.js +37 -8
- package/launcher/yakmesh.bat +62 -0
- package/launcher/yakmesh.sh +70 -0
- package/mesh/annex.js +462 -108
- package/mesh/beacon-broadcast.js +113 -1
- package/mesh/darshan.js +1718 -0
- package/mesh/gumba.js +1567 -0
- package/mesh/jhilke.js +651 -0
- package/mesh/katha.js +1012 -0
- package/mesh/nakpak-routing.js +8 -5
- package/mesh/network.js +724 -34
- package/mesh/pulse-sync.js +4 -1
- package/mesh/rate-limiter.js +127 -15
- package/mesh/seva.js +526 -0
- package/mesh/sherpa-discovery.js +89 -8
- package/mesh/sybil-defense.js +19 -5
- package/mesh/temporal-encoder.js +4 -3
- package/mesh/vani.js +1364 -0
- package/mesh/yurt.js +1340 -0
- package/models/entropy-sentinel.onnx +0 -0
- package/models/karma-trust.onnx +0 -0
- package/models/manifest.json +43 -0
- package/models/sakshi-anomaly.onnx +0 -0
- package/oracle/code-proof-protocol.js +7 -6
- package/oracle/codebase-lock.js +257 -28
- package/oracle/index.js +74 -15
- package/oracle/ma902-snmp.js +678 -0
- package/oracle/module-sealer.js +5 -3
- package/oracle/network-identity.js +16 -0
- package/oracle/packet-checksum.js +201 -0
- package/oracle/sst.js +579 -0
- package/oracle/ternary-144t.js +714 -0
- package/oracle/ternary-ml.js +481 -0
- package/oracle/time-api.js +239 -0
- package/oracle/time-source.js +137 -47
- package/oracle/validation-oracle-hardened.js +1111 -1071
- package/oracle/validation-oracle.js +4 -2
- package/oracle/ypc27.js +211 -0
- package/package.json +20 -3
- package/protocol/yak-handler.js +35 -9
- package/protocol/yak-protocol.js +28 -13
- package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
- package/reference/cpp/yakmesh_ypc27.cpp +179 -0
- package/sbom.json +87 -0
- package/scripts/security-audit.mjs +264 -0
- package/scripts/update-docs-nav.js +194 -0
- package/scripts/update-docs-sidebar.cjs +164 -0
- package/security/crypto-config.js +4 -3
- package/security/dharma-moderation.js +517 -0
- package/security/doko-identity.js +193 -143
- package/security/domain-consensus.js +86 -85
- package/security/fs-hardening.js +620 -0
- package/security/hardware-attestation.js +5 -3
- package/security/hybrid-trust.js +227 -87
- package/security/karma-rate-limiter.js +692 -0
- package/security/khata-protocol.js +22 -21
- package/security/khata-trust-integration.js +277 -150
- package/security/memory-safety.js +635 -0
- package/security/mesh-auth.js +11 -10
- package/security/mesh-revocation.js +373 -5
- package/security/namche-gateway.js +298 -69
- package/security/sakshi.js +460 -3
- package/security/sangha.js +770 -0
- package/security/secure-config.js +473 -0
- package/security/silicon-parity.js +13 -10
- package/security/steadywatch.js +1142 -0
- package/security/strike-system.js +32 -3
- package/security/temporal-signing.js +488 -0
- package/security/trit-commitment.js +464 -0
- package/server/crypto/annex.js +247 -0
- package/server/darshan-api.js +343 -0
- package/server/index.js +3259 -362
- package/server/komm-api.js +668 -0
- package/utils/accel.js +2273 -0
- package/utils/ternary-id.js +79 -0
- package/utils/verify-worker.js +57 -0
- package/webserver/index.js +95 -5
- package/assets/yakmesh-logo.png +0 -0
- package/assets/yakmesh-logo.svg +0 -80
- package/assets/yakmesh-logo2.png +0 -0
- package/assets/yakmesh-logo2sm.png +0 -0
- package/assets/ymsm.png +0 -0
- package/website/assets/silhouettes/adapters.svg +0 -107
- package/website/assets/silhouettes/api-endpoints.svg +0 -115
- package/website/assets/silhouettes/atomic-clock.svg +0 -83
- package/website/assets/silhouettes/base-camp.svg +0 -81
- package/website/assets/silhouettes/bridge.svg +0 -69
- package/website/assets/silhouettes/docs-bundle.svg +0 -113
- package/website/assets/silhouettes/doko-basket.svg +0 -70
- package/website/assets/silhouettes/fortress.svg +0 -93
- package/website/assets/silhouettes/gateway.svg +0 -54
- package/website/assets/silhouettes/gears.svg +0 -93
- package/website/assets/silhouettes/globe-satellite.svg +0 -67
- package/website/assets/silhouettes/karma-wheel.svg +0 -137
- package/website/assets/silhouettes/lama-council.svg +0 -141
- package/website/assets/silhouettes/mandala-network.svg +0 -169
- package/website/assets/silhouettes/mani-stones.svg +0 -149
- package/website/assets/silhouettes/mantra-wheel.svg +0 -116
- package/website/assets/silhouettes/mesh-nodes.svg +0 -113
- package/website/assets/silhouettes/nakpak.svg +0 -56
- package/website/assets/silhouettes/peak-lightning.svg +0 -73
- package/website/assets/silhouettes/sherpa.svg +0 -69
- package/website/assets/silhouettes/stupa-tower.svg +0 -119
- package/website/assets/silhouettes/tattva-eye.svg +0 -78
- package/website/assets/silhouettes/terminal.svg +0 -74
- package/website/assets/silhouettes/webserver.svg +0 -145
- package/website/assets/silhouettes/yak.svg +0 -78
- package/website/assets/yakmesh-logo.png +0 -0
- package/website/assets/yakmesh-logo.webp +0 -0
- package/website/assets/yakmesh-logo128x140.webp +0 -0
- package/website/assets/yakmesh-logo2.png +0 -0
- package/website/assets/yakmesh-logo2.svg +0 -51
- package/website/assets/yakmesh-logo40x44.webp +0 -0
- package/website/assets/yakmesh.gif +0 -0
- package/website/assets/yakmesh.ico +0 -0
- package/website/assets/yakmesh.jpg +0 -0
- package/website/assets/yakmesh.pdf +0 -0
- package/website/assets/yakmesh.png +0 -0
- package/website/assets/yakmesh.svg +0 -70
- package/website/assets/yakmesh128.webp +0 -0
- package/website/assets/yakmesh32.png +0 -0
- package/website/assets/yakmesh32.svg +0 -65
- package/website/assets/yakmesh32o.ico +0 -2
- package/website/assets/yakmesh32o.svg +0 -65
- package/website/assets/yakmesh32o.svgz +0 -0
package/gossip/protocol.js
CHANGED
|
@@ -23,12 +23,21 @@
|
|
|
23
23
|
* @version 2.6.0
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
26
|
+
import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
|
|
27
27
|
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
28
28
|
import { createLogger } from '../utils/logger.js';
|
|
29
29
|
|
|
30
|
+
// 144T ternary addressing for message IDs (eliminates hex "666" patterns)
|
|
31
|
+
import { TritAddress } from '../oracle/ternary-144t.js';
|
|
32
|
+
|
|
33
|
+
// ACCEL: Hardware-accelerated SHA3-256 (OpenSSL/SHA-NI — 4.6x faster)
|
|
34
|
+
import { sha3_256 } from '../utils/accel.js';
|
|
35
|
+
|
|
30
36
|
const log = createLogger('mantra:protocol');
|
|
31
37
|
|
|
38
|
+
/** Extract unique peer suffix from nodeId (e.g. 'node-net-name-pq-kEEU' → 'kEEU') */
|
|
39
|
+
const peerTag = (id) => id?.split('-pq-').pop() || id?.slice?.(-8) || String(id);
|
|
40
|
+
|
|
32
41
|
// Message types for MANTRA protocol
|
|
33
42
|
// (Maintains GOSSIP_ prefix for backward compatibility with existing mesh messages)
|
|
34
43
|
export const MantraMessageType = {
|
|
@@ -36,11 +45,11 @@ export const MantraMessageType = {
|
|
|
36
45
|
HELLO: 'GOSSIP_HELLO', // Announce self to network (prayer wheel spin)
|
|
37
46
|
PEERS: 'GOSSIP_PEERS', // Share known peers (community)
|
|
38
47
|
WANT_PEERS: 'GOSSIP_WANT_PEERS', // Request peer list (seeking guidance)
|
|
39
|
-
|
|
48
|
+
|
|
40
49
|
// Rumor mongering (MANTRA spreading)
|
|
41
50
|
RUMOR: 'GOSSIP_RUMOR', // New data to propagate (mantra to spread)
|
|
42
51
|
SEEN: 'GOSSIP_SEEN', // Acknowledge receipt (mantra received)
|
|
43
|
-
|
|
52
|
+
|
|
44
53
|
// Anti-entropy (KARMA balance)
|
|
45
54
|
DIGEST: 'GOSSIP_DIGEST', // Summary of known data (karma digest)
|
|
46
55
|
DIFF: 'GOSSIP_DIFF', // Missing data request (karma balance)
|
|
@@ -89,8 +98,9 @@ class BloomFilter {
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
// Reset when filter gets too full (false positive rate increases)
|
|
101
|
+
// 70% fill keeps FP rate manageable; 50% was too aggressive (wasted capacity)
|
|
92
102
|
shouldReset() {
|
|
93
|
-
return this.count > this.size * 0.
|
|
103
|
+
return this.count > this.size * 0.7;
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
reset() {
|
|
@@ -109,7 +119,14 @@ export class MantraProtocol {
|
|
|
109
119
|
constructor(mesh, identity, options = {}) {
|
|
110
120
|
this.mesh = mesh;
|
|
111
121
|
this.identity = identity;
|
|
112
|
-
|
|
122
|
+
|
|
123
|
+
// Relay info callback — lets gossip ask the node about active relay endpoints
|
|
124
|
+
// Returns { relayEndpoints: ['https://...'], relayNodeIds: Set } or null
|
|
125
|
+
this._getRelayInfo = options.getRelayInfo || null;
|
|
126
|
+
// Relay connect callback — lets gossip tell the node to register with a relay
|
|
127
|
+
// Signature: (relayEndpoint, nodeId) => Promise<void>
|
|
128
|
+
this._connectRelay = options.connectRelay || null;
|
|
129
|
+
|
|
113
130
|
// Configuration
|
|
114
131
|
this.config = {
|
|
115
132
|
fanout: options.fanout || 3, // Peers to spread mantra to
|
|
@@ -125,10 +142,15 @@ export class MantraProtocol {
|
|
|
125
142
|
this.knownPeers = new Map(); // nodeId -> { info, lastSeen, endpoint }
|
|
126
143
|
this.seenMessages = new BloomFilter();
|
|
127
144
|
this.pendingRumors = new Map(); // messageId -> { rumor, attempts, targets }
|
|
128
|
-
|
|
145
|
+
|
|
146
|
+
// Recent rumors buffer (for HTTP polling by MeshBridge)
|
|
147
|
+
this.recentRumors = []; // { topic, data, origin, timestamp, messageId }
|
|
148
|
+
this.maxRecentRumors = 500; // Keep last 500 rumors
|
|
149
|
+
this.rumorRetentionMs = 300000; // 5 min retention
|
|
150
|
+
|
|
129
151
|
// Intervals
|
|
130
152
|
this.intervals = [];
|
|
131
|
-
|
|
153
|
+
|
|
132
154
|
// Bind handlers
|
|
133
155
|
this._handleGossipMessage = this._handleGossipMessage.bind(this);
|
|
134
156
|
}
|
|
@@ -138,7 +160,7 @@ export class MantraProtocol {
|
|
|
138
160
|
*/
|
|
139
161
|
start() {
|
|
140
162
|
log.info('MANTRA protocol started - prayer wheel spinning');
|
|
141
|
-
|
|
163
|
+
|
|
142
164
|
// Register message handler with mesh
|
|
143
165
|
this.mesh.on('gossip', this._handleGossipMessage);
|
|
144
166
|
|
|
@@ -177,7 +199,7 @@ export class MantraProtocol {
|
|
|
177
199
|
*/
|
|
178
200
|
spreadRumor(topic, data) {
|
|
179
201
|
const messageId = this._generateMessageId(topic, data);
|
|
180
|
-
|
|
202
|
+
|
|
181
203
|
if (this.seenMessages.has(messageId)) {
|
|
182
204
|
return; // Already seen
|
|
183
205
|
}
|
|
@@ -192,31 +214,69 @@ export class MantraProtocol {
|
|
|
192
214
|
timestamp: Date.now(),
|
|
193
215
|
};
|
|
194
216
|
|
|
217
|
+
// Sign the rumor (ML-DSA-65) — excludes TTL since it decrements during propagation
|
|
218
|
+
const sigPayload = JSON.stringify({
|
|
219
|
+
messageId: rumor.messageId,
|
|
220
|
+
topic: rumor.topic,
|
|
221
|
+
data: rumor.data,
|
|
222
|
+
origin: rumor.origin,
|
|
223
|
+
timestamp: rumor.timestamp,
|
|
224
|
+
});
|
|
225
|
+
rumor.signature = this.identity.sign(sigPayload);
|
|
226
|
+
|
|
195
227
|
this.seenMessages.add(messageId);
|
|
228
|
+
this._bufferRumor(rumor);
|
|
196
229
|
this._propagateRumor(rumor);
|
|
197
|
-
|
|
230
|
+
|
|
231
|
+
// Also emit for HTTP relay bridge — locally-generated rumors must reach
|
|
232
|
+
// relay peers (nodes connected via HTTP polling, not WebSocket).
|
|
233
|
+
// _propagateRumor only sends to mesh.getPeers() (WS peers), so relay-only
|
|
234
|
+
// nodes (e.g. behind firewalls) would never propagate their own rumors
|
|
235
|
+
// without this. The server layer's outbound-gossip handler queues the
|
|
236
|
+
// message in the relay outbox for delivery on the next poll cycle.
|
|
237
|
+
this._emitForRelayBridge(rumor);
|
|
238
|
+
|
|
198
239
|
return messageId;
|
|
199
240
|
}
|
|
200
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Emit a rumor as an outbound-gossip event for the HTTP relay bridge.
|
|
244
|
+
* This ensures locally-generated rumors reach relay peers (nodes connected
|
|
245
|
+
* via HTTP polling rather than direct WebSocket). Without this, nodes that
|
|
246
|
+
* only have relay connections would generate rumors that never leave the node.
|
|
247
|
+
*/
|
|
248
|
+
_emitForRelayBridge(rumor) {
|
|
249
|
+
const meshMsg = {
|
|
250
|
+
type: 'gossip',
|
|
251
|
+
payload: { gossip: rumor },
|
|
252
|
+
id: rumor.messageId,
|
|
253
|
+
origin: rumor.origin,
|
|
254
|
+
ttl: rumor.ttl,
|
|
255
|
+
};
|
|
256
|
+
// Exclude our own nodeId — we're the origin, no need to relay back to self
|
|
257
|
+
this.mesh.emit('outbound-gossip', meshMsg, [rumor.origin]);
|
|
258
|
+
}
|
|
259
|
+
|
|
201
260
|
/**
|
|
202
261
|
* Get known peers (for peer discovery)
|
|
203
262
|
*/
|
|
204
263
|
getKnownPeers() {
|
|
205
264
|
const now = Date.now();
|
|
206
265
|
const peers = [];
|
|
207
|
-
|
|
266
|
+
|
|
208
267
|
for (const [nodeId, info] of this.knownPeers) {
|
|
209
268
|
if (now - info.lastSeen < this.config.peerTTL) {
|
|
210
269
|
peers.push({
|
|
211
270
|
nodeId,
|
|
212
271
|
name: info.name,
|
|
213
272
|
endpoint: info.endpoint,
|
|
273
|
+
relayEndpoints: info.relayEndpoints || [],
|
|
214
274
|
region: info.region,
|
|
215
275
|
lastSeen: info.lastSeen,
|
|
216
276
|
});
|
|
217
277
|
}
|
|
218
278
|
}
|
|
219
|
-
|
|
279
|
+
|
|
220
280
|
return peers;
|
|
221
281
|
}
|
|
222
282
|
|
|
@@ -260,14 +320,26 @@ export class MantraProtocol {
|
|
|
260
320
|
region: this.identity.identity.region,
|
|
261
321
|
capabilities: this.identity.identity.capabilities,
|
|
262
322
|
endpoint: this.mesh.getPublicEndpoint?.() || null,
|
|
323
|
+
publicKey: this.identity.identity.publicKey, // ML-DSA-65 public key for signature verification
|
|
263
324
|
timestamp: Date.now(),
|
|
264
325
|
};
|
|
265
326
|
|
|
327
|
+
// Include relay endpoints we're registered with so peers can discover relay paths
|
|
328
|
+
// This is how relay knowledge propagates organically through the mesh
|
|
329
|
+
if (this._getRelayInfo) {
|
|
330
|
+
try {
|
|
331
|
+
const relayInfo = this._getRelayInfo();
|
|
332
|
+
if (relayInfo?.relayEndpoints?.length > 0) {
|
|
333
|
+
hello.relayEndpoints = relayInfo.relayEndpoints;
|
|
334
|
+
}
|
|
335
|
+
} catch { /* relay info unavailable — that's ok */ }
|
|
336
|
+
}
|
|
337
|
+
|
|
266
338
|
this.mesh.broadcast({ gossip: hello });
|
|
267
|
-
|
|
339
|
+
|
|
268
340
|
// Also request peers
|
|
269
|
-
this.mesh.broadcast({
|
|
270
|
-
gossip: { type: GossipMessageType.WANT_PEERS }
|
|
341
|
+
this.mesh.broadcast({
|
|
342
|
+
gossip: { type: GossipMessageType.WANT_PEERS }
|
|
271
343
|
});
|
|
272
344
|
}
|
|
273
345
|
|
|
@@ -275,15 +347,38 @@ export class MantraProtocol {
|
|
|
275
347
|
* Handle HELLO from a peer
|
|
276
348
|
*/
|
|
277
349
|
_handleHello(message, fromNodeId) {
|
|
350
|
+
const isNewPeer = !this.knownPeers.has(message.nodeId);
|
|
351
|
+
|
|
278
352
|
this.knownPeers.set(message.nodeId, {
|
|
279
353
|
name: message.name,
|
|
280
354
|
region: message.region,
|
|
281
355
|
capabilities: message.capabilities,
|
|
282
356
|
endpoint: message.endpoint,
|
|
357
|
+
relayEndpoints: message.relayEndpoints || [],
|
|
358
|
+
publicKey: message.publicKey || null, // Store for gossip signature verification
|
|
283
359
|
lastSeen: Date.now(),
|
|
284
360
|
});
|
|
285
361
|
|
|
286
|
-
|
|
362
|
+
// Store public key in mesh._relayPeerKeys for signature verification
|
|
363
|
+
// This allows relay-discovered peers to verify each other's gossip signatures
|
|
364
|
+
if (message.publicKey && message.nodeId && this.mesh?._relayPeerKeys) {
|
|
365
|
+
this.mesh._relayPeerKeys.set(message.nodeId, message.publicKey);
|
|
366
|
+
} else if (message.publicKey && message.nodeId && this.mesh) {
|
|
367
|
+
// Initialize the map if it doesn't exist
|
|
368
|
+
if (!this.mesh._relayPeerKeys) this.mesh._relayPeerKeys = new Map();
|
|
369
|
+
this.mesh._relayPeerKeys.set(message.nodeId, message.publicKey);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Only log when we actually discover a new peer
|
|
373
|
+
if (isNewPeer) {
|
|
374
|
+
log.info('Discovered peer', { name: message.name, peer: peerTag(message.nodeId) });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// If peer advertises relay endpoints and we have no direct connection to the
|
|
378
|
+
// node behind that relay, auto-register — this is how relay knowledge spreads
|
|
379
|
+
if (message.relayEndpoints?.length > 0 && this._connectRelay) {
|
|
380
|
+
this._tryRelayConnect(message.relayEndpoints, message.nodeId);
|
|
381
|
+
}
|
|
287
382
|
|
|
288
383
|
// Respond with our peer list
|
|
289
384
|
this._sendPeerList(fromNodeId);
|
|
@@ -320,26 +415,54 @@ export class MantraProtocol {
|
|
|
320
415
|
_handlePeers(message) {
|
|
321
416
|
for (const peer of message.peers) {
|
|
322
417
|
if (peer.nodeId === this.identity.identity.nodeId) continue;
|
|
323
|
-
|
|
418
|
+
|
|
324
419
|
if (!this.knownPeers.has(peer.nodeId)) {
|
|
325
420
|
this.knownPeers.set(peer.nodeId, {
|
|
326
421
|
name: peer.name,
|
|
327
422
|
region: peer.region,
|
|
328
423
|
endpoint: peer.endpoint,
|
|
424
|
+
relayEndpoints: peer.relayEndpoints || [],
|
|
329
425
|
lastSeen: peer.lastSeen,
|
|
330
426
|
});
|
|
331
|
-
|
|
427
|
+
|
|
332
428
|
// Try to connect if we have an endpoint
|
|
333
429
|
if (peer.endpoint && !this.mesh.isConnectedTo(peer.nodeId)) {
|
|
334
430
|
log.debug('Attempting connection to discovered peer', { name: peer.name });
|
|
335
431
|
this.mesh.connectToPeer(peer.endpoint).catch(() => {
|
|
336
|
-
// Connection failed
|
|
432
|
+
// Connection failed — try relay if available
|
|
433
|
+
if (peer.relayEndpoints?.length > 0 && this._connectRelay) {
|
|
434
|
+
this._tryRelayConnect(peer.relayEndpoints, peer.nodeId);
|
|
435
|
+
}
|
|
337
436
|
});
|
|
437
|
+
} else if (!peer.endpoint && peer.relayEndpoints?.length > 0 && this._connectRelay) {
|
|
438
|
+
// No WS endpoint but has relay — connect via relay
|
|
439
|
+
this._tryRelayConnect(peer.relayEndpoints, peer.nodeId);
|
|
338
440
|
}
|
|
339
441
|
}
|
|
340
442
|
}
|
|
341
443
|
}
|
|
342
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Try connecting to a peer via relay endpoints discovered through gossip.
|
|
447
|
+
* Fire-and-forget — failure is silent (relay is a fallback, not primary path).
|
|
448
|
+
*/
|
|
449
|
+
_tryRelayConnect(relayEndpoints, nodeId) {
|
|
450
|
+
if (!this._connectRelay || !relayEndpoints?.length) return;
|
|
451
|
+
|
|
452
|
+
// Don't relay-connect to ourselves
|
|
453
|
+
if (nodeId === this.identity.identity.nodeId) return;
|
|
454
|
+
|
|
455
|
+
for (const endpoint of relayEndpoints) {
|
|
456
|
+
if (typeof endpoint !== 'string' || !endpoint.startsWith('http')) continue;
|
|
457
|
+
|
|
458
|
+
log.info(`GOSSIP relay discovery → ${endpoint} for ${peerTag(nodeId)}`);
|
|
459
|
+
this._connectRelay(endpoint, nodeId).catch(err => {
|
|
460
|
+
log.debug(`GOSSIP relay connect failed: ${endpoint} — ${err.message}`);
|
|
461
|
+
});
|
|
462
|
+
break; // Only try first viable relay endpoint
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
343
466
|
/**
|
|
344
467
|
* Handle incoming rumor
|
|
345
468
|
*/
|
|
@@ -355,18 +478,43 @@ export class MantraProtocol {
|
|
|
355
478
|
return;
|
|
356
479
|
}
|
|
357
480
|
|
|
481
|
+
// Verify origin's ML-DSA-65 signature before trusting the rumor
|
|
482
|
+
if (!rumor.signature) {
|
|
483
|
+
log.warn('Dropping unsigned rumor', { origin: peerTag(rumor.origin), messageId });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const originPubKey = this._getPeerPublicKey(rumor.origin);
|
|
487
|
+
if (!originPubKey) {
|
|
488
|
+
log.warn('Dropping rumor from unknown origin (no public key)', { origin: peerTag(rumor.origin), messageId });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const sigPayload = JSON.stringify({
|
|
492
|
+
messageId: rumor.messageId,
|
|
493
|
+
topic: rumor.topic,
|
|
494
|
+
data: rumor.data,
|
|
495
|
+
origin: rumor.origin,
|
|
496
|
+
timestamp: rumor.timestamp,
|
|
497
|
+
});
|
|
498
|
+
if (!this.identity.verify(sigPayload, rumor.signature, originPubKey)) {
|
|
499
|
+
log.warn('Dropping rumor with invalid signature', { origin: peerTag(rumor.origin), messageId });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
358
503
|
// Mark as seen
|
|
359
504
|
this.seenMessages.add(messageId);
|
|
360
|
-
|
|
505
|
+
|
|
361
506
|
// Check bloom filter health
|
|
362
507
|
if (this.seenMessages.shouldReset()) {
|
|
363
508
|
this.seenMessages.reset();
|
|
364
509
|
}
|
|
365
510
|
|
|
511
|
+
// Buffer for HTTP API consumers
|
|
512
|
+
this._bufferRumor(rumor);
|
|
513
|
+
|
|
366
514
|
// Emit event for application layer
|
|
367
515
|
this.mesh.emit('rumor', rumor.topic, rumor.data, rumor.origin);
|
|
368
516
|
|
|
369
|
-
log.debug('Received rumor', { topic: rumor.topic, origin: rumor.origin
|
|
517
|
+
log.debug('Received rumor', { topic: rumor.topic, origin: peerTag(rumor.origin) });
|
|
370
518
|
|
|
371
519
|
// Propagate if TTL allows
|
|
372
520
|
if (ttl > 1) {
|
|
@@ -396,13 +544,11 @@ export class MantraProtocol {
|
|
|
396
544
|
.filter(p => p.nodeId !== excludeNodeId && p.nodeId !== rumor.origin);
|
|
397
545
|
|
|
398
546
|
if (peers.length === 0) {
|
|
399
|
-
log.warn('No peers to propagate rumor to');
|
|
400
547
|
return;
|
|
401
548
|
}
|
|
402
549
|
|
|
403
550
|
// Select random subset based on fanout
|
|
404
551
|
const targets = this._selectRandom(peers, this.config.fanout);
|
|
405
|
-
log.debug('Propagating rumor', { targetCount: targets.length, targets: targets.map(t => t.nodeId.slice(0, 12)) });
|
|
406
552
|
|
|
407
553
|
for (const target of targets) {
|
|
408
554
|
// Use broadcast format so the mesh routes it correctly
|
|
@@ -434,7 +580,7 @@ export class MantraProtocol {
|
|
|
434
580
|
|
|
435
581
|
// Pick a random peer for anti-entropy
|
|
436
582
|
const target = peers[Math.floor(Math.random() * peers.length)];
|
|
437
|
-
|
|
583
|
+
|
|
438
584
|
this.mesh.sendTo(target.nodeId, {
|
|
439
585
|
gossip: { type: GossipMessageType.WANT_PEERS }
|
|
440
586
|
});
|
|
@@ -470,21 +616,95 @@ export class MantraProtocol {
|
|
|
470
616
|
}
|
|
471
617
|
|
|
472
618
|
/**
|
|
473
|
-
* Generate deterministic message ID
|
|
619
|
+
* Generate deterministic message ID using 144T ternary format
|
|
620
|
+
* Eliminates hex "666" patterns while maintaining collision resistance
|
|
621
|
+
* Returns tier 1 (36 trits) as compact string: "TT00TTT00:TTT00TTT0:0TTT00TTT:00TTT00TT"
|
|
474
622
|
*/
|
|
475
623
|
_generateMessageId(topic, data) {
|
|
476
624
|
const payload = JSON.stringify({ topic, data, origin: this.identity.identity.nodeId, ts: Date.now() });
|
|
477
|
-
|
|
625
|
+
const hex = bytesToHex(sha3_256(new TextEncoder().encode(payload)));
|
|
626
|
+
// Convert to 144T ternary address, extract tier 1 as compact string
|
|
627
|
+
const tritAddr = TritAddress.fromHex(hex);
|
|
628
|
+
return tritAddr.toString().split('.')[0]; // First tier only
|
|
478
629
|
}
|
|
479
630
|
|
|
480
631
|
/**
|
|
481
632
|
* Select random items from array
|
|
482
633
|
*/
|
|
483
634
|
_selectRandom(array, count) {
|
|
484
|
-
|
|
635
|
+
// Fisher-Yates shuffle — unbiased (sort-based shuffle is statistically skewed)
|
|
636
|
+
const shuffled = [...array];
|
|
637
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
638
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
639
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
640
|
+
}
|
|
485
641
|
return shuffled.slice(0, Math.min(count, array.length));
|
|
486
642
|
}
|
|
487
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Buffer a rumor for HTTP API retrieval
|
|
646
|
+
*/
|
|
647
|
+
_bufferRumor(rumor) {
|
|
648
|
+
this.recentRumors.push({
|
|
649
|
+
messageId: rumor.messageId,
|
|
650
|
+
topic: rumor.topic,
|
|
651
|
+
data: rumor.data,
|
|
652
|
+
origin: rumor.origin,
|
|
653
|
+
timestamp: rumor.timestamp || Date.now(),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Evict old entries
|
|
657
|
+
const cutoff = Date.now() - this.rumorRetentionMs;
|
|
658
|
+
while (this.recentRumors.length > this.maxRecentRumors ||
|
|
659
|
+
(this.recentRumors.length > 0 && this.recentRumors[0].timestamp < cutoff)) {
|
|
660
|
+
this.recentRumors.shift();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Get recent rumors (for HTTP API polling)
|
|
666
|
+
* @param {number} since - Timestamp to filter from (exclusive)
|
|
667
|
+
* @param {string} [topic] - Optional topic filter
|
|
668
|
+
* @returns {Array} Matching rumors
|
|
669
|
+
*/
|
|
670
|
+
getRecentRumors(since = 0, topic = null) {
|
|
671
|
+
return this.recentRumors.filter(r => {
|
|
672
|
+
if (r.timestamp <= since) return false;
|
|
673
|
+
if (topic && r.topic !== topic) return false;
|
|
674
|
+
return true;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Resolve a peer's public key from mesh state.
|
|
680
|
+
* Checks WS peers, relay keys, knownPeers, SHERPA registry, and self.
|
|
681
|
+
*/
|
|
682
|
+
_getPeerPublicKey(nodeId) {
|
|
683
|
+
// Self
|
|
684
|
+
if (nodeId === this.identity.identity.nodeId) {
|
|
685
|
+
return this.identity.identity.publicKey;
|
|
686
|
+
}
|
|
687
|
+
// WS peer info
|
|
688
|
+
if (this.mesh?.peers) {
|
|
689
|
+
const peer = this.mesh.peers.get(nodeId);
|
|
690
|
+
if (peer?.identity?.publicKey) return peer.identity.publicKey;
|
|
691
|
+
}
|
|
692
|
+
// Relay peer keys (stored during signed registration)
|
|
693
|
+
if (this.mesh?._relayPeerKeys) {
|
|
694
|
+
const key = this.mesh._relayPeerKeys.get(nodeId);
|
|
695
|
+
if (key) return key;
|
|
696
|
+
}
|
|
697
|
+
// knownPeers from HELLO messages (learned via gossip)
|
|
698
|
+
const knownPeer = this.knownPeers.get(nodeId);
|
|
699
|
+
if (knownPeer?.publicKey) return knownPeer.publicKey;
|
|
700
|
+
// SHERPA registry
|
|
701
|
+
if (this.mesh?.sherpa?.registry) {
|
|
702
|
+
const regPeer = this.mesh.sherpa.registry.get(nodeId);
|
|
703
|
+
if (regPeer?.publicKey) return regPeer.publicKey;
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
|
|
488
708
|
/**
|
|
489
709
|
* Get MANTRA statistics (prayer wheel metrics)
|
|
490
710
|
*/
|