@optimystic/db-p2p 0.1.1 → 0.1.2

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.
Files changed (64) hide show
  1. package/{readme.md → README.md} +7 -0
  2. package/dist/index.min.js +31 -30
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/cluster/cluster-repo.d.ts +27 -0
  5. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  6. package/dist/src/cluster/cluster-repo.js +129 -17
  7. package/dist/src/cluster/cluster-repo.js.map +1 -1
  8. package/dist/src/cluster/service.d.ts +13 -2
  9. package/dist/src/cluster/service.d.ts.map +1 -1
  10. package/dist/src/cluster/service.js +17 -7
  11. package/dist/src/cluster/service.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/libp2p-node.d.ts +13 -2
  17. package/dist/src/libp2p-node.d.ts.map +1 -1
  18. package/dist/src/libp2p-node.js +35 -16
  19. package/dist/src/libp2p-node.js.map +1 -1
  20. package/dist/src/protocol-client.d.ts.map +1 -1
  21. package/dist/src/protocol-client.js +8 -7
  22. package/dist/src/protocol-client.js.map +1 -1
  23. package/dist/src/repo/cluster-coordinator.d.ts +7 -2
  24. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  25. package/dist/src/repo/cluster-coordinator.js +18 -3
  26. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  27. package/dist/src/repo/coordinator-repo.d.ts +26 -3
  28. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  29. package/dist/src/repo/coordinator-repo.js +117 -22
  30. package/dist/src/repo/coordinator-repo.js.map +1 -1
  31. package/dist/src/repo/service.d.ts +13 -2
  32. package/dist/src/repo/service.d.ts.map +1 -1
  33. package/dist/src/repo/service.js +25 -12
  34. package/dist/src/repo/service.js.map +1 -1
  35. package/dist/src/storage/memory-storage.d.ts +15 -0
  36. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  37. package/dist/src/storage/memory-storage.js +23 -4
  38. package/dist/src/storage/memory-storage.js.map +1 -1
  39. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  40. package/dist/src/storage/storage-repo.js.map +1 -1
  41. package/dist/src/sync/service.d.ts.map +1 -1
  42. package/dist/src/sync/service.js +7 -2
  43. package/dist/src/sync/service.js.map +1 -1
  44. package/package.json +27 -21
  45. package/src/cluster/cluster-repo.ts +828 -711
  46. package/src/cluster/service.ts +44 -31
  47. package/src/index.ts +1 -1
  48. package/src/libp2p-key-network.ts +334 -334
  49. package/src/libp2p-node.ts +371 -339
  50. package/src/network/network-manager-service.ts +334 -334
  51. package/src/protocol-client.ts +53 -54
  52. package/src/repo/client.ts +112 -112
  53. package/src/repo/cluster-coordinator.ts +613 -592
  54. package/src/repo/coordinator-repo.ts +269 -137
  55. package/src/repo/service.ts +237 -219
  56. package/src/storage/block-storage.ts +182 -182
  57. package/src/storage/memory-storage.ts +24 -5
  58. package/src/storage/storage-repo.ts +321 -320
  59. package/src/sync/service.ts +7 -6
  60. package/dist/src/storage/file-storage.d.ts +0 -30
  61. package/dist/src/storage/file-storage.d.ts.map +0 -1
  62. package/dist/src/storage/file-storage.js +0 -127
  63. package/dist/src/storage/file-storage.js.map +0 -1
  64. package/src/storage/file-storage.ts +0 -163
@@ -1,339 +1,371 @@
1
- import { createLibp2p, type Libp2p } from 'libp2p';
2
- import { tcp } from '@libp2p/tcp';
3
- import { noise } from '@chainsafe/libp2p-noise';
4
- import { yamux } from '@chainsafe/libp2p-yamux';
5
- import { identify } from '@libp2p/identify';
6
- import { ping } from '@libp2p/ping';
7
- import { gossipsub } from '@chainsafe/libp2p-gossipsub';
8
- import { bootstrap } from '@libp2p/bootstrap';
9
- import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2';
10
- import { peerIdFromString } from '@libp2p/peer-id';
11
- import { clusterService } from './cluster/service.js';
12
- import { repoService } from './repo/service.js';
13
- import { StorageRepo } from './storage/storage-repo.js';
14
- import { BlockStorage } from './storage/block-storage.js';
15
- import { MemoryRawStorage } from './storage/memory-storage.js';
16
- import { FileRawStorage } from './storage/file-storage.js';
17
- import type { IRawStorage } from './storage/i-raw-storage.js';
18
- import { clusterMember } from './cluster/cluster-repo.js';
19
- import { coordinatorRepo } from './repo/coordinator-repo.js';
20
- import { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
21
- import { ClusterClient } from './cluster/client.js';
22
- import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
23
- import { multiaddr } from '@multiformats/multiaddr';
24
- import { networkManagerService } from './network/network-manager-service.js';
25
- import { fretService, Libp2pFretService } from 'p2p-fret';
26
- import { syncService } from './sync/service.js';
27
- import { RestorationCoordinator } from './storage/restoration-coordinator-v2.js';
28
- import { RingSelector } from './storage/ring-selector.js';
29
- import { StorageMonitor } from './storage/storage-monitor.js';
30
- import type { StorageMonitorConfig } from './storage/storage-monitor.js';
31
- import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
32
- import type { RestoreCallback } from './storage/struct.js';
33
- import type { FretService } from 'p2p-fret';
34
- import { PartitionDetector } from './cluster/partition-detector.js';
35
-
36
- export type NodeOptions = {
37
- port: number;
38
- bootstrapNodes: string[];
39
- networkName: string;
40
- fretProfile?: 'edge' | 'core';
41
- id?: string; // optional peer id
42
- relay?: boolean; // enable relay service
43
- storageType?: 'memory' | 'file'; // storage backend type
44
- storagePath?: string; // path for file storage (required if storageType is 'file')
45
- clusterSize?: number; // desired cluster size per key
46
- clusterPolicy?: {
47
- allowDownsize?: boolean;
48
- sizeTolerance?: number; // acceptable relative difference (e.g. 0.5 = +/-50%)
49
- superMajorityThreshold?: number; // fraction of peers needed for super-majority (default: 0.67)
50
- };
51
-
52
- /** Arachnode storage configuration */
53
- arachnode?: {
54
- enableRingZulu?: boolean; // default: true
55
- storage?: StorageMonitorConfig;
56
- };
57
-
58
- /** Transaction validator for cluster consensus */
59
- validator?: ITransactionValidator;
60
- };
61
-
62
- export async function createLibp2pNode(options: NodeOptions): Promise<Libp2p> {
63
- // Create storage based on type
64
- const storageType = options.storageType ?? 'memory';
65
- let rawStorage: IRawStorage;
66
-
67
- if (storageType === 'file') {
68
- if (!options.storagePath) {
69
- throw new Error('storagePath is required when storageType is "file"');
70
- }
71
- rawStorage = new FileRawStorage(options.storagePath);
72
- } else {
73
- rawStorage = new MemoryRawStorage();
74
- }
75
-
76
- // Create placeholder restore callback (will be replaced after node starts)
77
- let restoreCallback: RestoreCallback = async (_blockId, _rev?) => {
78
- return undefined;
79
- };
80
-
81
- // Create shared storage layers with restoration callback
82
- const storageRepo = new StorageRepo((blockId) =>
83
- new BlockStorage(blockId, rawStorage, restoreCallback)
84
- );
85
-
86
- let clusterImpl: ICluster | undefined;
87
- let coordinatedRepo: IRepo | undefined;
88
-
89
- const clusterProxy: ICluster = {
90
- async update(record) {
91
- if (!clusterImpl) {
92
- throw new Error('ClusterMember not initialized');
93
- }
94
- return await clusterImpl.update(record);
95
- }
96
- };
97
-
98
- const repoProxy: IRepo = {
99
- async get(blockGets, options) {
100
- const target = coordinatedRepo ?? storageRepo;
101
- return await target.get(blockGets, options);
102
- },
103
- async pend(request, options) {
104
- const target = coordinatedRepo ?? storageRepo;
105
- return await target.pend(request, options);
106
- },
107
- async cancel(trxRef, options) {
108
- const target = coordinatedRepo ?? storageRepo;
109
- return await target.cancel(trxRef, options);
110
- },
111
- async commit(request, options) {
112
- const target = coordinatedRepo ?? storageRepo;
113
- return await target.commit(request, options);
114
- }
115
- };
116
-
117
- // Parse peer ID if provided
118
- const peerId = options.id ? await peerIdFromString(options.id) : undefined;
119
-
120
- const libp2pOptions: any = {
121
- start: false,
122
- ...(peerId ? { peerId } : {}),
123
- addresses: {
124
- listen: [`/ip4/0.0.0.0/tcp/${options.port}`]
125
- },
126
- connectionManager: {
127
- autoDial: true,
128
- minConnections: 1,
129
- maxConnections: 16,
130
- inboundConnectionUpgradeTimeout: 10_000,
131
- dialQueue: { concurrency: 2, attempts: 2 }
132
- },
133
- // Add circuitRelayTransport so this node can dial through relays
134
- transports: [tcp(), circuitRelayTransport()],
135
- connectionEncrypters: [noise()],
136
- streamMuxers: [yamux()],
137
- services: {
138
- identify: identify({
139
- protocolPrefix: `/optimystic/${options.networkName}`
140
- }),
141
- ping: ping(),
142
- pubsub: gossipsub({
143
- allowPublishToZeroTopicPeers: true,
144
- heartbeatInterval: 7000
145
- }),
146
- // Circuit relay server - enables this node to relay connections for other peers
147
- ...(options.relay ? { relay: circuitRelayServer() } : {}),
148
-
149
- // Custom services - create wrapper factories that inject dependencies
150
- cluster: (components: any) => {
151
- const serviceFactory = clusterService({
152
- protocolPrefix: `/optimystic/${options.networkName}`,
153
- configuredClusterSize: options.clusterSize ?? 10,
154
- allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
155
- clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5
156
- });
157
- return serviceFactory({
158
- logger: components.logger,
159
- registrar: components.registrar,
160
- cluster: clusterProxy
161
- });
162
- },
163
-
164
- repo: (components: any) => {
165
- const serviceFactory = repoService({
166
- protocolPrefix: `/optimystic/${options.networkName}`
167
- });
168
- return serviceFactory({
169
- logger: components.logger,
170
- registrar: components.registrar,
171
- repo: repoProxy
172
- });
173
- },
174
-
175
- sync: (components: any) => {
176
- const serviceFactory = syncService({
177
- protocolPrefix: `/optimystic/${options.networkName}`
178
- });
179
- return serviceFactory({
180
- logger: components.logger,
181
- registrar: components.registrar,
182
- repo: repoProxy
183
- });
184
- },
185
-
186
- networkManager: (components: any) => {
187
- const svcFactory = networkManagerService({
188
- clusterSize: options.clusterSize ?? 10,
189
- expectedRemotes: (options.bootstrapNodes?.length ?? 0) > 0,
190
- allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
191
- clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5
192
- })
193
- const svc = svcFactory(components)
194
- try { (svc as any).setLibp2p?.(components.libp2p) } catch { }
195
- return svc
196
- },
197
- fret: (components: any) => {
198
- const svcFactory = fretService({
199
- k: 15,
200
- m: 8,
201
- capacity: 2048,
202
- profile: options.fretProfile ?? ((options.bootstrapNodes?.length ?? 0) > 0 ? 'core' : 'edge'),
203
- networkName: options.networkName,
204
- bootstraps: options.bootstrapNodes ?? []
205
- });
206
- const svc = svcFactory(components) as Libp2pFretService;
207
- try { svc.setLibp2p(components.libp2p); } catch { }
208
- return svc;
209
- }
210
- },
211
- // Add bootstrap nodes as needed
212
- peerDiscovery: [
213
- ...(options.bootstrapNodes?.length ? [bootstrap({ list: options.bootstrapNodes })] : [])
214
- ],
215
- };
216
-
217
- const node = await createLibp2p(libp2pOptions);
218
-
219
- // Inject libp2p reference into services that need it before start
220
- try { ((node as any).services?.fret as any)?.setLibp2p?.(node) } catch { }
221
- try { ((node as any).services?.networkManager as any)?.setLibp2p?.(node) } catch { }
222
-
223
- await node.start();
224
-
225
- // Initialize cluster coordination components
226
- const keyNetwork = new Libp2pKeyPeerNetwork(node);
227
- const protocolPrefix = `/optimystic/${options.networkName}`;
228
- const createClusterClient = (peerId: any) => ClusterClient.create(peerId, keyNetwork, protocolPrefix);
229
-
230
- // Create partition detector and get FRET service
231
- const partitionDetector = new PartitionDetector();
232
- const fretSvc = (node as any).services?.fret as FretService | undefined;
233
-
234
- clusterImpl = clusterMember({
235
- storageRepo,
236
- peerNetwork: keyNetwork,
237
- peerId: node.peerId,
238
- protocolPrefix,
239
- partitionDetector,
240
- fretService: fretSvc,
241
- validator: options.validator
242
- });
243
-
244
- const coordinatorRepoFactory = coordinatorRepo(
245
- keyNetwork,
246
- createClusterClient,
247
- {
248
- clusterSize: options.clusterSize ?? 10,
249
- superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? 0.67,
250
- simpleMajorityThreshold: 0.51,
251
- minAbsoluteClusterSize: 2, // Allow 2-node clusters for development/small networks
252
- allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
253
- clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
254
- partitionDetectionWindow: 60000
255
- },
256
- fretSvc
257
- );
258
-
259
- coordinatedRepo = coordinatorRepoFactory({
260
- storageRepo,
261
- localCluster: clusterImpl,
262
- localPeerId: node.peerId
263
- });
264
-
265
- // Initialize Arachnode ring membership and restoration
266
- const enableArachnode = options.arachnode?.enableRingZulu ?? true;
267
- if (enableArachnode) {
268
- const log = (node as any).logger?.forComponent?.('db-p2p:arachnode');
269
- const fret = (node as any).services?.fret as any;
270
-
271
- if (fret) {
272
- const fretAdapter = new ArachnodeFretAdapter(fret);
273
-
274
- const storageMonitor = new StorageMonitor(rawStorage, options.arachnode?.storage ?? {});
275
- const ringSelector = new RingSelector(fretAdapter, storageMonitor, {
276
- minCapacity: 100 * 1024 * 1024, // 100MB minimum
277
- thresholds: {
278
- moveOut: 0.85,
279
- moveIn: 0.40
280
- }
281
- });
282
-
283
- // Determine and announce ring membership
284
- const peerId = node.peerId.toString();
285
- const arachnodeInfo = await ringSelector.createArachnodeInfo(peerId);
286
- fretAdapter.setArachnodeInfo(arachnodeInfo);
287
-
288
- log?.('Announced Arachnode membership: Ring %d', arachnodeInfo.ringDepth);
289
-
290
- // Setup restoration coordinator with FRET adapter
291
- const restorationCoordinatorV2 = new RestorationCoordinator(
292
- fretAdapter,
293
- { connect: (pid, protocol) => node.dialProtocol(pid, [protocol]) },
294
- `/optimystic/${options.networkName}`
295
- );
296
-
297
- // Update restore callback to use new coordinator
298
- const newRestoreCallback: RestoreCallback = async (blockId, rev?) => {
299
- return await restorationCoordinatorV2.restore(blockId, rev);
300
- };
301
-
302
- // Replace the restore callback (this is a bit hacky, but works for now)
303
- // In production, we'd want to properly manage this
304
- (storageRepo as any).createBlockStorage = (blockId: string) =>
305
- new BlockStorage(blockId, rawStorage, newRestoreCallback);
306
-
307
- // Monitor capacity and adjust ring periodically
308
- const monitorInterval = setInterval(async () => {
309
- const transition = await ringSelector.shouldTransition();
310
- if (transition.shouldMove) {
311
- log?.('Ring transition needed: moving %s to Ring %d',
312
- transition.direction, transition.newRingDepth);
313
-
314
- // Update Arachnode info with new ring
315
- const updatedInfo = await ringSelector.createArachnodeInfo(peerId);
316
- fretAdapter.setArachnodeInfo(updatedInfo);
317
- }
318
- }, 60_000); // Check every minute
319
-
320
- // Cleanup on node stop
321
- const originalStop = node.stop.bind(node);
322
- node.stop = async () => {
323
- clearInterval(monitorInterval);
324
- await originalStop();
325
- };
326
- } else {
327
- log?.('FRET service not available, Arachnode disabled');
328
- }
329
- }
330
-
331
- // Skip proactive bootstrap dials; rely on discovery and minimal churn
332
-
333
- // Expose coordinated repo and storage for external use
334
- (node as any).coordinatedRepo = coordinatedRepo;
335
- (node as any).storageRepo = storageRepo;
336
- (node as any).keyNetwork = keyNetwork;
337
-
338
- return node;
339
- }
1
+ import { createLibp2p, type Libp2p } from 'libp2p';
2
+ import { tcp } from '@libp2p/tcp';
3
+ import { noise } from '@chainsafe/libp2p-noise';
4
+ import { yamux } from '@chainsafe/libp2p-yamux';
5
+ import { identify } from '@libp2p/identify';
6
+ import { ping } from '@libp2p/ping';
7
+ import { gossipsub } from '@chainsafe/libp2p-gossipsub';
8
+ import { bootstrap } from '@libp2p/bootstrap';
9
+ import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2';
10
+ import { peerIdFromString } from '@libp2p/peer-id';
11
+ import { clusterService } from './cluster/service.js';
12
+ import { repoService } from './repo/service.js';
13
+ import { StorageRepo } from './storage/storage-repo.js';
14
+ import { BlockStorage } from './storage/block-storage.js';
15
+ import { MemoryRawStorage } from './storage/memory-storage.js';
16
+ import type { IRawStorage } from './storage/i-raw-storage.js';
17
+ import { clusterMember } from './cluster/cluster-repo.js';
18
+ import { coordinatorRepo } from './repo/coordinator-repo.js';
19
+ import { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
20
+ import { ClusterClient } from './cluster/client.js';
21
+ import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
22
+ import { networkManagerService } from './network/network-manager-service.js';
23
+ import { fretService, Libp2pFretService } from 'p2p-fret';
24
+ import { syncService } from './sync/service.js';
25
+ import { SyncClient } from './sync/client.js';
26
+ import type { ClusterLatestCallback } from './repo/coordinator-repo.js';
27
+ import { RestorationCoordinator } from './storage/restoration-coordinator-v2.js';
28
+ import { RingSelector } from './storage/ring-selector.js';
29
+ import { StorageMonitor } from './storage/storage-monitor.js';
30
+ import type { StorageMonitorConfig } from './storage/storage-monitor.js';
31
+ import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
32
+ import type { RestoreCallback } from './storage/struct.js';
33
+ import type { FretService } from 'p2p-fret';
34
+ import { PartitionDetector } from './cluster/partition-detector.js';
35
+
36
+ /** Factory function or instance for creating raw storage */
37
+ export type RawStorageProvider = IRawStorage | (() => IRawStorage);
38
+
39
+ export type NodeOptions = {
40
+ port: number;
41
+ bootstrapNodes: string[];
42
+ networkName: string;
43
+ fretProfile?: 'edge' | 'core';
44
+ id?: string; // optional peer id
45
+ relay?: boolean; // enable relay service
46
+ /** Storage provider - either an IRawStorage instance or a factory function. Defaults to MemoryRawStorage if not provided. */
47
+ storage?: RawStorageProvider;
48
+ clusterSize?: number; // desired cluster size per key
49
+ clusterPolicy?: {
50
+ allowDownsize?: boolean;
51
+ sizeTolerance?: number; // acceptable relative difference (e.g. 0.5 = +/-50%)
52
+ superMajorityThreshold?: number; // fraction of peers needed for super-majority (default: 0.67)
53
+ };
54
+
55
+ /**
56
+ * Responsibility K - the replica set size for determining cluster membership.
57
+ * This is distinct from kBucketSize (DHT routing) and clusterSize (consensus quorum).
58
+ * When a node receives a request, it checks if it's in the top responsibilityK
59
+ * peers (by XOR distance) for the key. If not, it redirects to closer peers.
60
+ * Default: 1 (only the closest peer is responsible)
61
+ */
62
+ responsibilityK?: number;
63
+
64
+ /** Arachnode storage configuration */
65
+ arachnode?: {
66
+ enableRingZulu?: boolean; // default: true
67
+ storage?: StorageMonitorConfig;
68
+ };
69
+
70
+ /** Transaction validator for cluster consensus */
71
+ validator?: ITransactionValidator;
72
+ };
73
+
74
+ function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
75
+ if (!provider) {
76
+ return new MemoryRawStorage();
77
+ }
78
+ return typeof provider === 'function' ? provider() : provider;
79
+ }
80
+
81
+ export async function createLibp2pNode(options: NodeOptions): Promise<Libp2p> {
82
+ const rawStorage = resolveStorage(options.storage);
83
+
84
+ // Create placeholder restore callback (will be replaced after node starts)
85
+ let restoreCallback: RestoreCallback = async (_blockId, _rev?) => {
86
+ return undefined;
87
+ };
88
+
89
+ // Create shared storage layers with restoration callback
90
+ const storageRepo = new StorageRepo((blockId) =>
91
+ new BlockStorage(blockId, rawStorage, restoreCallback)
92
+ );
93
+
94
+ let clusterImpl: ICluster | undefined;
95
+ let coordinatedRepo: IRepo | undefined;
96
+
97
+ const clusterProxy: ICluster = {
98
+ async update(record) {
99
+ if (!clusterImpl) {
100
+ throw new Error('ClusterMember not initialized');
101
+ }
102
+ return await clusterImpl.update(record);
103
+ }
104
+ };
105
+
106
+ const repoProxy: IRepo = {
107
+ async get(blockGets, options) {
108
+ const target = coordinatedRepo ?? storageRepo;
109
+ return await target.get(blockGets, options);
110
+ },
111
+ async pend(request, options) {
112
+ const target = coordinatedRepo ?? storageRepo;
113
+ return await target.pend(request, options);
114
+ },
115
+ async cancel(trxRef, options) {
116
+ const target = coordinatedRepo ?? storageRepo;
117
+ return await target.cancel(trxRef, options);
118
+ },
119
+ async commit(request, options) {
120
+ const target = coordinatedRepo ?? storageRepo;
121
+ return await target.commit(request, options);
122
+ }
123
+ };
124
+
125
+ // Parse peer ID if provided
126
+ const peerId = options.id ? await peerIdFromString(options.id) : undefined;
127
+
128
+ const libp2pOptions: any = {
129
+ start: false,
130
+ ...(peerId ? { peerId } : {}),
131
+ addresses: {
132
+ listen: [`/ip4/0.0.0.0/tcp/${options.port}`]
133
+ },
134
+ connectionManager: {
135
+ autoDial: true,
136
+ minConnections: 1,
137
+ maxConnections: 16,
138
+ inboundConnectionUpgradeTimeout: 10_000,
139
+ dialQueue: { concurrency: 2, attempts: 2 }
140
+ },
141
+ // Add circuitRelayTransport so this node can dial through relays
142
+ transports: [tcp(), circuitRelayTransport()],
143
+ connectionEncrypters: [noise()],
144
+ streamMuxers: [yamux()],
145
+ services: {
146
+ identify: identify({
147
+ protocolPrefix: `/optimystic/${options.networkName}`
148
+ }),
149
+ ping: ping(),
150
+ pubsub: gossipsub({
151
+ allowPublishToZeroTopicPeers: true,
152
+ heartbeatInterval: 7000
153
+ }),
154
+ // Circuit relay server - enables this node to relay connections for other peers
155
+ ...(options.relay ? { relay: circuitRelayServer() } : {}),
156
+
157
+ // Custom services - create wrapper factories that inject dependencies
158
+ cluster: (components: any) => {
159
+ const serviceFactory = clusterService({
160
+ protocolPrefix: `/optimystic/${options.networkName}`,
161
+ configuredClusterSize: options.clusterSize ?? 10,
162
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
163
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
164
+ responsibilityK: options.responsibilityK ?? 1
165
+ });
166
+ return serviceFactory({
167
+ logger: components.logger,
168
+ registrar: components.registrar,
169
+ cluster: clusterProxy
170
+ });
171
+ },
172
+
173
+ repo: (components: any) => {
174
+ const serviceFactory = repoService({
175
+ protocolPrefix: `/optimystic/${options.networkName}`,
176
+ responsibilityK: options.responsibilityK ?? 1
177
+ });
178
+ return serviceFactory({
179
+ logger: components.logger,
180
+ registrar: components.registrar,
181
+ repo: repoProxy
182
+ });
183
+ },
184
+
185
+ sync: (components: any) => {
186
+ const serviceFactory = syncService({
187
+ protocolPrefix: `/optimystic/${options.networkName}`
188
+ });
189
+ return serviceFactory({
190
+ logger: components.logger,
191
+ registrar: components.registrar,
192
+ repo: repoProxy
193
+ });
194
+ },
195
+
196
+ networkManager: (components: any) => {
197
+ const svcFactory = networkManagerService({
198
+ clusterSize: options.clusterSize ?? 10,
199
+ expectedRemotes: (options.bootstrapNodes?.length ?? 0) > 0,
200
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
201
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5
202
+ })
203
+ const svc = svcFactory(components)
204
+ try { (svc as any).setLibp2p?.(components.libp2p) } catch { }
205
+ return svc
206
+ },
207
+ fret: (components: any) => {
208
+ const svcFactory = fretService({
209
+ k: 15,
210
+ m: 8,
211
+ capacity: 2048,
212
+ profile: options.fretProfile ?? ((options.bootstrapNodes?.length ?? 0) > 0 ? 'core' : 'edge'),
213
+ networkName: options.networkName,
214
+ bootstraps: options.bootstrapNodes ?? []
215
+ });
216
+ const svc = svcFactory(components) as Libp2pFretService;
217
+ try { svc.setLibp2p(components.libp2p); } catch { }
218
+ return svc;
219
+ }
220
+ },
221
+ // Add bootstrap nodes as needed
222
+ peerDiscovery: [
223
+ ...(options.bootstrapNodes?.length ? [bootstrap({ list: options.bootstrapNodes })] : [])
224
+ ],
225
+ };
226
+
227
+ const node = await createLibp2p(libp2pOptions);
228
+
229
+ // Inject libp2p reference into services that need it before start
230
+ try { ((node as any).services?.fret as any)?.setLibp2p?.(node) } catch { }
231
+ try { ((node as any).services?.networkManager as any)?.setLibp2p?.(node) } catch { }
232
+
233
+ await node.start();
234
+
235
+ // Initialize cluster coordination components
236
+ const keyNetwork = new Libp2pKeyPeerNetwork(node);
237
+ const protocolPrefix = `/optimystic/${options.networkName}`;
238
+ const createClusterClient = (peerId: any) => ClusterClient.create(peerId, keyNetwork, protocolPrefix);
239
+
240
+ // Create partition detector and get FRET service
241
+ const partitionDetector = new PartitionDetector();
242
+ const fretSvc = (node as any).services?.fret as FretService | undefined;
243
+
244
+ clusterImpl = clusterMember({
245
+ storageRepo,
246
+ peerNetwork: keyNetwork,
247
+ peerId: node.peerId,
248
+ protocolPrefix,
249
+ partitionDetector,
250
+ fretService: fretSvc,
251
+ validator: options.validator
252
+ });
253
+
254
+ const coordinatorRepoFactory = coordinatorRepo(
255
+ keyNetwork,
256
+ createClusterClient,
257
+ {
258
+ clusterSize: options.clusterSize ?? 10,
259
+ superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? 0.67,
260
+ simpleMajorityThreshold: 0.51,
261
+ minAbsoluteClusterSize: 2, // Allow 2-node clusters for development/small networks
262
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
263
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
264
+ partitionDetectionWindow: 60000
265
+ },
266
+ fretSvc
267
+ );
268
+
269
+ // Create callback for querying cluster peers for their latest block revision
270
+ const clusterLatestCallback: ClusterLatestCallback = async (peerId, blockId) => {
271
+ const syncClient = new SyncClient(peerId, keyNetwork, protocolPrefix);
272
+ try {
273
+ const response = await syncClient.requestBlock({ blockId, rev: undefined });
274
+ if (response.success && response.archive) {
275
+ const revisions = Object.keys(response.archive.revisions).map(Number);
276
+ if (revisions.length > 0) {
277
+ const maxRev = Math.max(...revisions);
278
+ const revisionData = response.archive.revisions[maxRev];
279
+ if (revisionData?.action) {
280
+ return { actionId: revisionData.action.actionId, rev: maxRev };
281
+ }
282
+ }
283
+ }
284
+ } catch {
285
+ // Peer may be unreachable - return undefined to skip this peer
286
+ }
287
+ return undefined;
288
+ };
289
+
290
+ coordinatedRepo = coordinatorRepoFactory({
291
+ storageRepo,
292
+ localCluster: clusterImpl,
293
+ localPeerId: node.peerId,
294
+ clusterLatestCallback
295
+ });
296
+
297
+ // Initialize Arachnode ring membership and restoration
298
+ const enableArachnode = options.arachnode?.enableRingZulu ?? true;
299
+ if (enableArachnode) {
300
+ const log = (node as any).logger?.forComponent?.('db-p2p:arachnode');
301
+ const fret = (node as any).services?.fret as any;
302
+
303
+ if (fret) {
304
+ const fretAdapter = new ArachnodeFretAdapter(fret);
305
+
306
+ const storageMonitor = new StorageMonitor(rawStorage, options.arachnode?.storage ?? {});
307
+ const ringSelector = new RingSelector(fretAdapter, storageMonitor, {
308
+ minCapacity: 100 * 1024 * 1024, // 100MB minimum
309
+ thresholds: {
310
+ moveOut: 0.85,
311
+ moveIn: 0.40
312
+ }
313
+ });
314
+
315
+ // Determine and announce ring membership
316
+ const peerId = node.peerId.toString();
317
+ const arachnodeInfo = await ringSelector.createArachnodeInfo(peerId);
318
+ fretAdapter.setArachnodeInfo(arachnodeInfo);
319
+
320
+ log?.('Announced Arachnode membership: Ring %d', arachnodeInfo.ringDepth);
321
+
322
+ // Setup restoration coordinator with FRET adapter
323
+ const restorationCoordinatorV2 = new RestorationCoordinator(
324
+ fretAdapter,
325
+ { connect: (pid, protocol) => node.dialProtocol(pid, [protocol]) },
326
+ `/optimystic/${options.networkName}`
327
+ );
328
+
329
+ // Update restore callback to use new coordinator
330
+ const newRestoreCallback: RestoreCallback = async (blockId, rev?) => {
331
+ return await restorationCoordinatorV2.restore(blockId, rev);
332
+ };
333
+
334
+ // Replace the restore callback (this is a bit hacky, but works for now)
335
+ // In production, we'd want to properly manage this
336
+ (storageRepo as any).createBlockStorage = (blockId: string) =>
337
+ new BlockStorage(blockId, rawStorage, newRestoreCallback);
338
+
339
+ // Monitor capacity and adjust ring periodically
340
+ const monitorInterval = setInterval(async () => {
341
+ const transition = await ringSelector.shouldTransition();
342
+ if (transition.shouldMove) {
343
+ log?.('Ring transition needed: moving %s to Ring %d',
344
+ transition.direction, transition.newRingDepth);
345
+
346
+ // Update Arachnode info with new ring
347
+ const updatedInfo = await ringSelector.createArachnodeInfo(peerId);
348
+ fretAdapter.setArachnodeInfo(updatedInfo);
349
+ }
350
+ }, 60_000); // Check every minute
351
+
352
+ // Cleanup on node stop
353
+ const originalStop = node.stop.bind(node);
354
+ node.stop = async () => {
355
+ clearInterval(monitorInterval);
356
+ await originalStop();
357
+ };
358
+ } else {
359
+ log?.('FRET service not available, Arachnode disabled');
360
+ }
361
+ }
362
+
363
+ // Skip proactive bootstrap dials; rely on discovery and minimal churn
364
+
365
+ // Expose coordinated repo and storage for external use
366
+ (node as any).coordinatedRepo = coordinatedRepo;
367
+ (node as any).storageRepo = storageRepo;
368
+ (node as any).keyNetwork = keyNetwork;
369
+
370
+ return node;
371
+ }