@highway1/core 0.1.39 → 0.1.41
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/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +18 -4
- package/src/discovery/agent-card-encoder.ts +119 -0
- package/src/discovery/agent-card-schema.ts +87 -0
- package/src/discovery/agent-card-types.ts +99 -0
- package/src/discovery/agent-card.ts +190 -0
- package/src/discovery/bootstrap.ts +63 -0
- package/src/discovery/capability-matcher.ts +167 -0
- package/src/discovery/dht.ts +285 -0
- package/src/discovery/index.ts +3 -0
- package/src/discovery/search-index.ts +247 -0
- package/src/discovery/semantic-search.ts +218 -0
- package/src/identity/did.ts +48 -0
- package/src/identity/document.ts +77 -0
- package/src/identity/index.ts +4 -0
- package/src/identity/keys.ts +79 -0
- package/src/identity/signer.ts +55 -0
- package/src/index.ts +33 -0
- package/src/messaging/codec.ts +47 -0
- package/src/messaging/envelope.ts +107 -0
- package/src/messaging/index.ts +3 -0
- package/src/messaging/router.ts +368 -0
- package/src/transport/connection.ts +77 -0
- package/src/transport/index.ts +2 -0
- package/src/transport/node.ts +152 -0
- package/src/trust/endorsement.ts +167 -0
- package/src/trust/index.ts +194 -0
- package/src/trust/interaction-history.ts +155 -0
- package/src/trust/sybil-defense.ts +232 -0
- package/src/trust/trust-score.ts +136 -0
- package/src/utils/errors.ts +38 -0
- package/src/utils/logger.ts +48 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import { multiaddr } from '@multiformats/multiaddr';
|
|
4
|
+
import type { MessageEnvelope } from './envelope.js';
|
|
5
|
+
import type { DHTOperations } from '../discovery/dht.js';
|
|
6
|
+
import { encodeMessage, decodeMessage } from './codec.js';
|
|
7
|
+
import { validateEnvelope, verifyEnvelope } from './envelope.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
import { MessagingError } from '../utils/errors.js';
|
|
10
|
+
import { extractPublicKey } from '../identity/did.js';
|
|
11
|
+
import { verify } from '../identity/keys.js';
|
|
12
|
+
|
|
13
|
+
const logger = createLogger('router');
|
|
14
|
+
|
|
15
|
+
export type MessageHandler = (
|
|
16
|
+
envelope: MessageEnvelope
|
|
17
|
+
) => Promise<MessageEnvelope | void>;
|
|
18
|
+
|
|
19
|
+
export interface PeerHint {
|
|
20
|
+
peerId: string;
|
|
21
|
+
multiaddrs: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MessageRouter {
|
|
25
|
+
registerHandler: (protocol: string, handler: MessageHandler) => void;
|
|
26
|
+
unregisterHandler: (protocol: string) => void;
|
|
27
|
+
registerCatchAllHandler: (handler: MessageHandler) => void;
|
|
28
|
+
sendMessage: (envelope: MessageEnvelope, peerHint?: PeerHint) => Promise<MessageEnvelope | void>;
|
|
29
|
+
start: () => Promise<void>;
|
|
30
|
+
stop: () => Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a message router for a libp2p node.
|
|
35
|
+
* When dht is provided, sendMessage can resolve DIDs to peer addresses via DHT lookup.
|
|
36
|
+
* When relayPeers is provided, sendMessage will attempt relay fallback if direct dial fails.
|
|
37
|
+
*/
|
|
38
|
+
export function createMessageRouter(
|
|
39
|
+
libp2p: Libp2p,
|
|
40
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>,
|
|
41
|
+
dht?: DHTOperations,
|
|
42
|
+
relayPeers?: string[]
|
|
43
|
+
): MessageRouter {
|
|
44
|
+
const handlers = new Map<string, MessageHandler>();
|
|
45
|
+
let catchAllHandler: MessageHandler | undefined;
|
|
46
|
+
const PROTOCOL_PREFIX = '/clawiverse/msg/1.0.0';
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
registerHandler: (protocol: string, handler: MessageHandler) => {
|
|
50
|
+
handlers.set(protocol, handler);
|
|
51
|
+
logger.info('Registered message handler', { protocol });
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
unregisterHandler: (protocol: string) => {
|
|
55
|
+
handlers.delete(protocol);
|
|
56
|
+
logger.info('Unregistered message handler', { protocol });
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
registerCatchAllHandler: (handler: MessageHandler) => {
|
|
60
|
+
catchAllHandler = handler;
|
|
61
|
+
logger.info('Registered catch-all message handler');
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
sendMessage: async (envelope: MessageEnvelope, peerHint?: PeerHint) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!validateEnvelope(envelope)) {
|
|
67
|
+
throw new MessagingError('Invalid message envelope');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Resolve peer: try peerHint first, then DHT
|
|
71
|
+
let targetPeerIdStr: string | undefined;
|
|
72
|
+
let targetMultiaddrs: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (peerHint) {
|
|
75
|
+
targetPeerIdStr = peerHint.peerId;
|
|
76
|
+
targetMultiaddrs = peerHint.multiaddrs;
|
|
77
|
+
logger.info('Using peer hint for direct addressing', { peerId: targetPeerIdStr });
|
|
78
|
+
} else if (dht) {
|
|
79
|
+
const resolved = await dht.resolveDID(envelope.to);
|
|
80
|
+
if (resolved) {
|
|
81
|
+
targetPeerIdStr = resolved.peerId;
|
|
82
|
+
targetMultiaddrs = resolved.multiaddrs;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!targetPeerIdStr) {
|
|
87
|
+
throw new MessagingError(
|
|
88
|
+
`Cannot resolve recipient: ${envelope.to} — provide peerHint or ensure agent is in DHT`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const targetPeerId = peerIdFromString(targetPeerIdStr);
|
|
93
|
+
|
|
94
|
+
if (targetMultiaddrs.length > 0) {
|
|
95
|
+
const mas = targetMultiaddrs.map((a) => multiaddr(a));
|
|
96
|
+
await libp2p.peerStore.merge(targetPeerId, { multiaddrs: mas });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.info('Dialing peer for message delivery', {
|
|
100
|
+
peerId: targetPeerIdStr,
|
|
101
|
+
multiaddrs: targetMultiaddrs,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let stream: any;
|
|
105
|
+
|
|
106
|
+
// Separate relay addrs from direct addrs
|
|
107
|
+
const relayMultiaddrs = targetMultiaddrs.filter((a) => a.includes('/p2p-circuit/'));
|
|
108
|
+
const directMultiaddrs = targetMultiaddrs.filter((a) => !a.includes('/p2p-circuit/'));
|
|
109
|
+
|
|
110
|
+
// Try direct addresses first
|
|
111
|
+
for (const addr of directMultiaddrs) {
|
|
112
|
+
try {
|
|
113
|
+
const conn = await libp2p.dial(multiaddr(addr));
|
|
114
|
+
stream = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
115
|
+
logger.info('Direct dial succeeded', { addr });
|
|
116
|
+
break;
|
|
117
|
+
} catch {
|
|
118
|
+
// try next
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let lastError: unknown;
|
|
123
|
+
|
|
124
|
+
// Fall back to relay: dial the circuit addr to establish connection, then newStream
|
|
125
|
+
if (!stream) {
|
|
126
|
+
const allRelayAddrs = [
|
|
127
|
+
...relayMultiaddrs,
|
|
128
|
+
...(relayPeers ?? []).map((r) => buildCircuitRelayAddr(r, targetPeerIdStr!)),
|
|
129
|
+
];
|
|
130
|
+
// Deduplicate
|
|
131
|
+
const uniqueRelayAddrs = [...new Set(allRelayAddrs)];
|
|
132
|
+
for (const addr of uniqueRelayAddrs) {
|
|
133
|
+
try {
|
|
134
|
+
// Step 1: establish the relay connection
|
|
135
|
+
const conn = await libp2p.dial(multiaddr(addr));
|
|
136
|
+
logger.info('Relay connection established', { addr });
|
|
137
|
+
// Step 2: open a protocol stream on the relay (limited) connection
|
|
138
|
+
stream = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
139
|
+
logger.info('Relay stream opened', { addr });
|
|
140
|
+
break;
|
|
141
|
+
} catch (relayErr) {
|
|
142
|
+
logger.warn('Relay dial failed', { addr, error: (relayErr as Error).message });
|
|
143
|
+
lastError = relayErr;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Last resort: query DHT for relay-capable peers
|
|
149
|
+
if (!stream && dht && 'queryRelayPeers' in dht) {
|
|
150
|
+
const discoveredRelays = await (dht as any).queryRelayPeers();
|
|
151
|
+
for (const relayAddr of discoveredRelays) {
|
|
152
|
+
const circuitAddr = buildCircuitRelayAddr(relayAddr, targetPeerIdStr!);
|
|
153
|
+
try {
|
|
154
|
+
const conn = await libp2p.dial(multiaddr(circuitAddr));
|
|
155
|
+
stream = await conn.newStream(PROTOCOL_PREFIX, { runOnLimitedConnection: true });
|
|
156
|
+
logger.info('DHT-discovered relay succeeded', { relayAddr });
|
|
157
|
+
break;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
logger.warn('DHT relay failed', { relayAddr, error: (e as Error).message });
|
|
160
|
+
lastError = e;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!stream) {
|
|
166
|
+
throw lastError ?? new MessagingError('All dial attempts failed (including DHT-discovered relays)');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const encoded = encodeMessage(envelope);
|
|
170
|
+
await stream.sink(
|
|
171
|
+
(async function* () {
|
|
172
|
+
yield encoded;
|
|
173
|
+
})()
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
logger.info('Message sent over libp2p stream', {
|
|
177
|
+
id: envelope.id,
|
|
178
|
+
from: envelope.from,
|
|
179
|
+
to: envelope.to,
|
|
180
|
+
protocol: envelope.protocol,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// If this is a request, wait for response
|
|
184
|
+
if (envelope.type === 'request') {
|
|
185
|
+
logger.debug('Waiting for response to request', { id: envelope.id });
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Add timeout to prevent infinite blocking
|
|
189
|
+
const RESPONSE_TIMEOUT = 30000; // 30 seconds
|
|
190
|
+
|
|
191
|
+
const responsePromise = (async () => {
|
|
192
|
+
const responseChunks: Uint8Array[] = [];
|
|
193
|
+
for await (const chunk of stream.source) {
|
|
194
|
+
responseChunks.push(chunk.subarray());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (responseChunks.length > 0) {
|
|
198
|
+
const responseData = new Uint8Array(
|
|
199
|
+
responseChunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
|
200
|
+
);
|
|
201
|
+
let offset = 0;
|
|
202
|
+
for (const chunk of responseChunks) {
|
|
203
|
+
responseData.set(chunk, offset);
|
|
204
|
+
offset += chunk.length;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const responseEnvelope = decodeMessage(responseData);
|
|
208
|
+
logger.info('Received response', {
|
|
209
|
+
id: responseEnvelope.id,
|
|
210
|
+
replyTo: responseEnvelope.replyTo,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return responseEnvelope;
|
|
214
|
+
} else {
|
|
215
|
+
logger.warn('No response received for request', { id: envelope.id });
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
})();
|
|
219
|
+
|
|
220
|
+
const timeoutPromise = new Promise<undefined>((resolve) => {
|
|
221
|
+
setTimeout(() => {
|
|
222
|
+
logger.warn('Response timeout', { id: envelope.id, timeout: RESPONSE_TIMEOUT });
|
|
223
|
+
resolve(undefined);
|
|
224
|
+
}, RESPONSE_TIMEOUT);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
|
228
|
+
return response;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
logger.warn('Error reading response', { error });
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return undefined;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof MessagingError) throw error;
|
|
238
|
+
throw new MessagingError('Failed to send message', error);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
start: async () => {
|
|
243
|
+
await libp2p.handle(PROTOCOL_PREFIX, async ({ stream }) => {
|
|
244
|
+
try {
|
|
245
|
+
await handleIncomingStream(stream, handlers, catchAllHandler, verifyFn);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error('Error handling incoming stream', error);
|
|
248
|
+
}
|
|
249
|
+
}, { runOnLimitedConnection: true });
|
|
250
|
+
|
|
251
|
+
logger.info('Message router started');
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
stop: async () => {
|
|
255
|
+
await libp2p.unhandle(PROTOCOL_PREFIX);
|
|
256
|
+
handlers.clear();
|
|
257
|
+
catchAllHandler = undefined;
|
|
258
|
+
logger.info('Message router stopped');
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function buildCircuitRelayAddr(relayAddr: string, targetPeerId: string): string {
|
|
264
|
+
return `${relayAddr}/p2p-circuit/p2p/${targetPeerId}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleIncomingStream(
|
|
268
|
+
stream: any,
|
|
269
|
+
handlers: Map<string, MessageHandler>,
|
|
270
|
+
catchAllHandler: MessageHandler | undefined,
|
|
271
|
+
verifyFn: (signature: Uint8Array, data: Uint8Array) => Promise<boolean>
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
try {
|
|
274
|
+
const chunks: Uint8Array[] = [];
|
|
275
|
+
for await (const chunk of stream.source) {
|
|
276
|
+
chunks.push(chunk.subarray());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const data = new Uint8Array(
|
|
280
|
+
chunks.reduce((acc, chunk) => acc + chunk.length, 0)
|
|
281
|
+
);
|
|
282
|
+
let offset = 0;
|
|
283
|
+
for (const chunk of chunks) {
|
|
284
|
+
data.set(chunk, offset);
|
|
285
|
+
offset += chunk.length;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const envelope = decodeMessage(data);
|
|
289
|
+
|
|
290
|
+
if (!validateEnvelope(envelope)) {
|
|
291
|
+
logger.warn('Received invalid message envelope');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Verify signature against sender DID embedded public key.
|
|
296
|
+
const isValidSignature = await verifyEnvelope(envelope, async (signature, data) => {
|
|
297
|
+
const senderPublicKey = extractPublicKey(envelope.from);
|
|
298
|
+
return verify(signature, data, senderPublicKey);
|
|
299
|
+
});
|
|
300
|
+
if (!isValidSignature) {
|
|
301
|
+
logger.warn('Received message with invalid signature', {
|
|
302
|
+
id: envelope.id,
|
|
303
|
+
from: envelope.from,
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Optional caller-supplied verification hook (e.g. policy checks).
|
|
309
|
+
try {
|
|
310
|
+
const { signature, ...envelopeWithoutSig } = envelope;
|
|
311
|
+
const dataBytes = new TextEncoder().encode(JSON.stringify(envelopeWithoutSig));
|
|
312
|
+
const signatureBytes = Buffer.from(signature, 'hex');
|
|
313
|
+
const hookValid = await verifyFn(signatureBytes, dataBytes);
|
|
314
|
+
if (!hookValid) {
|
|
315
|
+
logger.warn('Message rejected by custom verifier', {
|
|
316
|
+
id: envelope.id,
|
|
317
|
+
from: envelope.from,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logger.warn('Custom verification hook failed', {
|
|
323
|
+
id: envelope.id,
|
|
324
|
+
from: envelope.from,
|
|
325
|
+
error: (error as Error).message,
|
|
326
|
+
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logger.info('Received message', {
|
|
331
|
+
id: envelope.id,
|
|
332
|
+
from: envelope.from,
|
|
333
|
+
to: envelope.to,
|
|
334
|
+
protocol: envelope.protocol,
|
|
335
|
+
payload: envelope.payload,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const handler = handlers.get(envelope.protocol);
|
|
339
|
+
let response: MessageEnvelope | void = undefined;
|
|
340
|
+
|
|
341
|
+
if (handler) {
|
|
342
|
+
response = await handler(envelope);
|
|
343
|
+
} else if (catchAllHandler) {
|
|
344
|
+
logger.debug('Using catch-all handler for protocol', { protocol: envelope.protocol });
|
|
345
|
+
response = await catchAllHandler(envelope);
|
|
346
|
+
} else {
|
|
347
|
+
logger.warn('No handler for protocol', { protocol: envelope.protocol });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Send response back if handler returned one
|
|
351
|
+
if (response) {
|
|
352
|
+
const encoded = encodeMessage(response);
|
|
353
|
+
logger.info('Sending response back to sender', {
|
|
354
|
+
responseId: response.id,
|
|
355
|
+
replyTo: response.replyTo,
|
|
356
|
+
size: encoded.length
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await stream.sink(
|
|
360
|
+
(async function* () {
|
|
361
|
+
yield encoded;
|
|
362
|
+
})()
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
logger.error('Error handling incoming message', error);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Libp2p } from 'libp2p';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
import { TransportError } from '../utils/errors.js';
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('connection');
|
|
6
|
+
|
|
7
|
+
export interface ConnectionManager {
|
|
8
|
+
connect: (peerId: string) => Promise<void>;
|
|
9
|
+
disconnect: (peerId: string) => Promise<void>;
|
|
10
|
+
isConnected: (peerId: string) => boolean;
|
|
11
|
+
getConnectedPeers: () => string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a connection manager for a libp2p node
|
|
16
|
+
*/
|
|
17
|
+
export function createConnectionManager(
|
|
18
|
+
libp2p: Libp2p
|
|
19
|
+
): ConnectionManager {
|
|
20
|
+
return {
|
|
21
|
+
connect: async (peerIdStr: string) => {
|
|
22
|
+
try {
|
|
23
|
+
// Parse peer ID and multiaddr
|
|
24
|
+
const connections = libp2p.getConnections();
|
|
25
|
+
const existing = connections.find(
|
|
26
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (existing) {
|
|
30
|
+
logger.debug('Already connected to peer', { peerId: peerIdStr });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// In a real implementation, we would dial the peer
|
|
35
|
+
// For now, we rely on libp2p's automatic connection management
|
|
36
|
+
logger.info('Connecting to peer', { peerId: peerIdStr });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new TransportError('Failed to connect to peer', error);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
disconnect: async (peerIdStr: string) => {
|
|
43
|
+
try {
|
|
44
|
+
const connections = libp2p.getConnections();
|
|
45
|
+
const matching = connections.filter(
|
|
46
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
for (const conn of matching) {
|
|
50
|
+
await conn.close();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logger.info('Disconnected from peer', { peerId: peerIdStr });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new TransportError('Failed to disconnect from peer', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
isConnected: (peerIdStr: string) => {
|
|
60
|
+
const connections = libp2p.getConnections();
|
|
61
|
+
return connections.some(
|
|
62
|
+
(conn) => conn.remotePeer.toString() === peerIdStr
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getConnectedPeers: () => {
|
|
67
|
+
const connections = libp2p.getConnections();
|
|
68
|
+
const peers = new Set<string>();
|
|
69
|
+
|
|
70
|
+
for (const conn of connections) {
|
|
71
|
+
peers.add(conn.remotePeer.toString());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Array.from(peers);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createLibp2p, Libp2p } from 'libp2p';
|
|
2
|
+
import { tcp } from '@libp2p/tcp';
|
|
3
|
+
import { noise } from '@chainsafe/libp2p-noise';
|
|
4
|
+
import { mplex } from '@libp2p/mplex';
|
|
5
|
+
import { kadDHT, passthroughMapper } from '@libp2p/kad-dht';
|
|
6
|
+
import { bootstrap } from '@libp2p/bootstrap';
|
|
7
|
+
import { identify } from '@libp2p/identify';
|
|
8
|
+
import { ping } from '@libp2p/ping';
|
|
9
|
+
import {
|
|
10
|
+
circuitRelayTransport,
|
|
11
|
+
circuitRelayServer,
|
|
12
|
+
} from '@libp2p/circuit-relay-v2';
|
|
13
|
+
import { createLogger } from '../utils/logger.js';
|
|
14
|
+
import { TransportError } from '../utils/errors.js';
|
|
15
|
+
import type { KeyPair } from '../identity/keys.js';
|
|
16
|
+
import type { PrivateKey } from '@libp2p/interface';
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('transport');
|
|
19
|
+
|
|
20
|
+
export interface TransportConfig {
|
|
21
|
+
keyPair?: KeyPair;
|
|
22
|
+
listenAddresses?: string[];
|
|
23
|
+
bootstrapPeers?: string[];
|
|
24
|
+
enableDHT?: boolean;
|
|
25
|
+
/** Run as a relay server so NAT'd agents can receive messages through this node */
|
|
26
|
+
enableRelay?: boolean;
|
|
27
|
+
/** Reserve a relay slot on bootstrap nodes so others can reach us (only needed for hw1 join) */
|
|
28
|
+
reserveRelaySlot?: boolean;
|
|
29
|
+
/** Enable mDNS local peer discovery (default: false) */
|
|
30
|
+
enableMDNS?: boolean;
|
|
31
|
+
/** libp2p PrivateKey for persistent PeerID (from @libp2p/crypto) */
|
|
32
|
+
privateKey?: PrivateKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ClawiverseNode {
|
|
36
|
+
libp2p: Libp2p;
|
|
37
|
+
start: () => Promise<void>;
|
|
38
|
+
stop: () => Promise<void>;
|
|
39
|
+
getMultiaddrs: () => string[];
|
|
40
|
+
getPeerId: () => string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a Clawiverse transport node with libp2p
|
|
45
|
+
*/
|
|
46
|
+
export async function createNode(
|
|
47
|
+
config: TransportConfig
|
|
48
|
+
): Promise<ClawiverseNode> {
|
|
49
|
+
try {
|
|
50
|
+
const {
|
|
51
|
+
listenAddresses = ['/ip4/0.0.0.0/tcp/0'],
|
|
52
|
+
bootstrapPeers = [],
|
|
53
|
+
enableDHT = true,
|
|
54
|
+
enableRelay = false,
|
|
55
|
+
reserveRelaySlot = false,
|
|
56
|
+
enableMDNS = false,
|
|
57
|
+
privateKey,
|
|
58
|
+
} = config;
|
|
59
|
+
|
|
60
|
+
// Reserve relay slots by adding p2p-circuit listen addresses (for hw1 join)
|
|
61
|
+
const relayListenAddrs = reserveRelaySlot
|
|
62
|
+
? bootstrapPeers.map((peer) => `${peer}/p2p-circuit`)
|
|
63
|
+
: [];
|
|
64
|
+
|
|
65
|
+
const libp2pConfig: any = {
|
|
66
|
+
addresses: {
|
|
67
|
+
listen: [...listenAddresses, ...relayListenAddrs],
|
|
68
|
+
},
|
|
69
|
+
...(privateKey ? { privateKey } : {}),
|
|
70
|
+
transports: [
|
|
71
|
+
tcp(),
|
|
72
|
+
circuitRelayTransport(),
|
|
73
|
+
],
|
|
74
|
+
connectionEncrypters: [noise()],
|
|
75
|
+
streamMuxers: [mplex()],
|
|
76
|
+
connectionManager: {
|
|
77
|
+
minConnections: bootstrapPeers.length > 0 ? 1 : 0,
|
|
78
|
+
maxConnections: 50,
|
|
79
|
+
},
|
|
80
|
+
services: {
|
|
81
|
+
identify: identify(),
|
|
82
|
+
ping: ping(),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (enableRelay) {
|
|
87
|
+
libp2pConfig.services.relay = circuitRelayServer({
|
|
88
|
+
reservations: {
|
|
89
|
+
maxReservations: 100, // Allow up to 100 concurrent relay reservations
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enableDHT) {
|
|
95
|
+
libp2pConfig.services.dht = kadDHT({
|
|
96
|
+
clientMode: false,
|
|
97
|
+
peerInfoMapper: passthroughMapper,
|
|
98
|
+
validators: {
|
|
99
|
+
clawiverse: async () => {},
|
|
100
|
+
},
|
|
101
|
+
selectors: {
|
|
102
|
+
clawiverse: () => 0,
|
|
103
|
+
},
|
|
104
|
+
// Optimize for small networks: reduce replication factor and query timeout
|
|
105
|
+
kBucketSize: 20, // Default K=20, keep for compatibility
|
|
106
|
+
querySelfInterval: 300000, // Self-query every 5 min (default 30s)
|
|
107
|
+
// Allow queries to complete faster in small networks
|
|
108
|
+
allowQueryWithZeroPeers: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const peerDiscovery: any[] = [];
|
|
113
|
+
if (bootstrapPeers.length > 0) peerDiscovery.push(bootstrap({ list: bootstrapPeers }));
|
|
114
|
+
if (enableMDNS) {
|
|
115
|
+
const { mdns } = await import('@libp2p/mdns');
|
|
116
|
+
peerDiscovery.push(mdns());
|
|
117
|
+
}
|
|
118
|
+
if (peerDiscovery.length > 0) libp2pConfig.peerDiscovery = peerDiscovery;
|
|
119
|
+
|
|
120
|
+
const libp2p = await createLibp2p(libp2pConfig);
|
|
121
|
+
|
|
122
|
+
logger.info('Libp2p node created', {
|
|
123
|
+
listenAddresses: libp2pConfig.addresses.listen,
|
|
124
|
+
reserveRelaySlot,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
libp2p,
|
|
129
|
+
start: async () => {
|
|
130
|
+
await libp2p.start();
|
|
131
|
+
logger.info('Node started', {
|
|
132
|
+
peerId: libp2p.peerId.toString(),
|
|
133
|
+
addresses: libp2p.getMultiaddrs().map((ma) => ma.toString()),
|
|
134
|
+
relay: enableRelay,
|
|
135
|
+
reserveRelaySlot,
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
stop: async () => {
|
|
139
|
+
await libp2p.stop();
|
|
140
|
+
logger.info('Node stopped');
|
|
141
|
+
},
|
|
142
|
+
getMultiaddrs: () => {
|
|
143
|
+
return libp2p.getMultiaddrs().map((ma) => ma.toString());
|
|
144
|
+
},
|
|
145
|
+
getPeerId: () => {
|
|
146
|
+
return libp2p.peerId.toString();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new TransportError('Failed to create transport node', error);
|
|
151
|
+
}
|
|
152
|
+
}
|