@optimystic/db-p2p 0.2.3 → 0.3.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.
Files changed (139) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +66 -0
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -0
  3. package/dist/src/cluster/block-transfer-service.js +163 -0
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -0
  5. package/dist/src/cluster/block-transfer.d.ts +79 -0
  6. package/dist/src/cluster/block-transfer.d.ts.map +1 -0
  7. package/dist/src/cluster/block-transfer.js +211 -0
  8. package/dist/src/cluster/block-transfer.js.map +1 -0
  9. package/dist/src/cluster/cluster-repo.d.ts +14 -3
  10. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  11. package/dist/src/cluster/cluster-repo.js +80 -35
  12. package/dist/src/cluster/cluster-repo.js.map +1 -1
  13. package/dist/src/cluster/rebalance-monitor.d.ts +64 -0
  14. package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -0
  15. package/dist/src/cluster/rebalance-monitor.js +159 -0
  16. package/dist/src/cluster/rebalance-monitor.js.map +1 -0
  17. package/dist/src/cluster/service.js +1 -1
  18. package/dist/src/cluster/service.js.map +1 -1
  19. package/dist/src/dispute/arbitrator-selection.d.ts +10 -0
  20. package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -0
  21. package/dist/src/dispute/arbitrator-selection.js +22 -0
  22. package/dist/src/dispute/arbitrator-selection.js.map +1 -0
  23. package/dist/src/dispute/client.d.ts +17 -0
  24. package/dist/src/dispute/client.d.ts.map +1 -0
  25. package/dist/src/dispute/client.js +28 -0
  26. package/dist/src/dispute/client.js.map +1 -0
  27. package/dist/src/dispute/dispute-service.d.ts +81 -0
  28. package/dist/src/dispute/dispute-service.d.ts.map +1 -0
  29. package/dist/src/dispute/dispute-service.js +365 -0
  30. package/dist/src/dispute/dispute-service.js.map +1 -0
  31. package/dist/src/dispute/engine-health-monitor.d.ts +22 -0
  32. package/dist/src/dispute/engine-health-monitor.d.ts.map +1 -0
  33. package/dist/src/dispute/engine-health-monitor.js +75 -0
  34. package/dist/src/dispute/engine-health-monitor.js.map +1 -0
  35. package/dist/src/dispute/index.d.ts +7 -0
  36. package/dist/src/dispute/index.d.ts.map +1 -0
  37. package/dist/src/dispute/index.js +7 -0
  38. package/dist/src/dispute/index.js.map +1 -0
  39. package/dist/src/dispute/service.d.ts +41 -0
  40. package/dist/src/dispute/service.d.ts.map +1 -0
  41. package/dist/src/dispute/service.js +82 -0
  42. package/dist/src/dispute/service.js.map +1 -0
  43. package/dist/src/dispute/types.d.ts +106 -0
  44. package/dist/src/dispute/types.d.ts.map +1 -0
  45. package/dist/src/dispute/types.js +7 -0
  46. package/dist/src/dispute/types.js.map +1 -0
  47. package/dist/src/index.d.ts +3 -0
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/dist/src/index.js +3 -0
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/libp2p-key-network.d.ts +23 -2
  52. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  53. package/dist/src/libp2p-key-network.js +100 -15
  54. package/dist/src/libp2p-key-network.js.map +1 -1
  55. package/dist/src/libp2p-node-base.d.ts +6 -0
  56. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  57. package/dist/src/libp2p-node-base.js +66 -12
  58. package/dist/src/libp2p-node-base.js.map +1 -1
  59. package/dist/src/logger.d.ts +1 -0
  60. package/dist/src/logger.d.ts.map +1 -1
  61. package/dist/src/logger.js +2 -0
  62. package/dist/src/logger.js.map +1 -1
  63. package/dist/src/network/network-manager-service.d.ts +15 -4
  64. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  65. package/dist/src/network/network-manager-service.js +33 -20
  66. package/dist/src/network/network-manager-service.js.map +1 -1
  67. package/dist/src/protocol-client.d.ts +1 -0
  68. package/dist/src/protocol-client.d.ts.map +1 -1
  69. package/dist/src/protocol-client.js +23 -2
  70. package/dist/src/protocol-client.js.map +1 -1
  71. package/dist/src/repo/client.d.ts +1 -0
  72. package/dist/src/repo/client.d.ts.map +1 -1
  73. package/dist/src/repo/client.js +18 -1
  74. package/dist/src/repo/client.js.map +1 -1
  75. package/dist/src/repo/cluster-coordinator.d.ts +3 -1
  76. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  77. package/dist/src/repo/cluster-coordinator.js +42 -2
  78. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  79. package/dist/src/repo/coordinator-repo.d.ts +18 -2
  80. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  81. package/dist/src/repo/coordinator-repo.js +62 -6
  82. package/dist/src/repo/coordinator-repo.js.map +1 -1
  83. package/dist/src/repo/service.d.ts +18 -2
  84. package/dist/src/repo/service.d.ts.map +1 -1
  85. package/dist/src/repo/service.js +88 -91
  86. package/dist/src/repo/service.js.map +1 -1
  87. package/dist/src/reputation/index.d.ts +3 -0
  88. package/dist/src/reputation/index.d.ts.map +1 -0
  89. package/dist/src/reputation/index.js +3 -0
  90. package/dist/src/reputation/index.js.map +1 -0
  91. package/dist/src/reputation/peer-reputation.d.ts +23 -0
  92. package/dist/src/reputation/peer-reputation.d.ts.map +1 -0
  93. package/dist/src/reputation/peer-reputation.js +121 -0
  94. package/dist/src/reputation/peer-reputation.js.map +1 -0
  95. package/dist/src/reputation/types.d.ts +89 -0
  96. package/dist/src/reputation/types.d.ts.map +1 -0
  97. package/dist/src/reputation/types.js +42 -0
  98. package/dist/src/reputation/types.js.map +1 -0
  99. package/dist/src/storage/arachnode-fret-adapter.d.ts +5 -0
  100. package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
  101. package/dist/src/storage/arachnode-fret-adapter.js +10 -0
  102. package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
  103. package/dist/src/storage/block-storage.d.ts.map +1 -1
  104. package/dist/src/storage/block-storage.js +5 -0
  105. package/dist/src/storage/block-storage.js.map +1 -1
  106. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  107. package/dist/src/storage/storage-repo.js +8 -0
  108. package/dist/src/storage/storage-repo.js.map +1 -1
  109. package/package.json +11 -10
  110. package/src/cluster/block-transfer-service.ts +228 -0
  111. package/src/cluster/block-transfer.ts +284 -0
  112. package/src/cluster/cluster-repo.ts +93 -38
  113. package/src/cluster/rebalance-monitor.ts +225 -0
  114. package/src/dispute/arbitrator-selection.ts +28 -0
  115. package/src/dispute/client.ts +41 -0
  116. package/src/dispute/dispute-service.ts +453 -0
  117. package/src/dispute/engine-health-monitor.ts +86 -0
  118. package/src/dispute/index.ts +17 -0
  119. package/src/dispute/service.ts +119 -0
  120. package/src/dispute/types.ts +114 -0
  121. package/src/index.ts +3 -0
  122. package/src/libp2p-key-network.ts +120 -22
  123. package/src/libp2p-node-base.ts +77 -13
  124. package/src/logger.ts +2 -1
  125. package/src/network/network-manager-service.ts +47 -16
  126. package/src/protocol-client.ts +29 -7
  127. package/src/repo/client.ts +20 -6
  128. package/src/repo/cluster-coordinator.ts +43 -2
  129. package/src/repo/coordinator-repo.ts +70 -7
  130. package/src/repo/redirect.ts +0 -2
  131. package/src/repo/service.ts +95 -87
  132. package/src/reputation/index.ts +12 -0
  133. package/src/reputation/peer-reputation.ts +147 -0
  134. package/src/reputation/types.ts +117 -0
  135. package/src/storage/arachnode-fret-adapter.ts +11 -0
  136. package/src/storage/block-storage.ts +6 -0
  137. package/src/storage/storage-repo.ts +9 -0
  138. package/dist/index.min.js +0 -53
  139. package/dist/index.min.js.map +0 -7
@@ -0,0 +1,114 @@
1
+ import type { ClusterRecord } from '@optimystic/db-core';
2
+
3
+ /** Evidence a validator collects during transaction re-execution */
4
+ export type ValidationEvidence = {
5
+ /** Operations hash the validator computed */
6
+ computedHash: string;
7
+ /** Engine used for validation */
8
+ engineId: string;
9
+ /** Schema hash at time of validation */
10
+ schemaHash: string;
11
+ /** Snapshot of block states during validation */
12
+ blockStateHashes: {
13
+ [blockId: string]: { revision: number; contentHash: string };
14
+ };
15
+ };
16
+
17
+ /** A challenge initiated by an overridden minority peer */
18
+ export type DisputeChallenge = {
19
+ /** Hash of (messageHash + challengerPeerId + timestamp) */
20
+ disputeId: string;
21
+ /** References the disputed ClusterRecord */
22
+ originalMessageHash: string;
23
+ /** Full record including all promises */
24
+ originalRecord: ClusterRecord;
25
+ /** Peer ID of the challenger */
26
+ challengerPeerId: string;
27
+ /** Challenger's validation evidence */
28
+ challengerEvidence: ValidationEvidence;
29
+ /** Challenger signs the dispute */
30
+ signature: string;
31
+ /** Timestamp of dispute creation */
32
+ timestamp: number;
33
+ /** TTL for arbitration (default: 2 × transaction TTL) */
34
+ expiration: number;
35
+ };
36
+
37
+ /** An arbitrator's independent assessment */
38
+ export type ArbitrationVote = {
39
+ /** Dispute being voted on */
40
+ disputeId: string;
41
+ /** Peer ID of the arbitrator */
42
+ arbitratorPeerId: string;
43
+ /** The arbitrator's verdict */
44
+ vote: 'agree-with-challenger' | 'agree-with-majority' | 'inconclusive';
45
+ /** Arbitrator's own re-execution results */
46
+ evidence: ValidationEvidence;
47
+ /** Arbitrator signs the vote */
48
+ signature: string;
49
+ };
50
+
51
+ /** Final resolution of a dispute */
52
+ export type DisputeResolution = {
53
+ /** Dispute being resolved */
54
+ disputeId: string;
55
+ /** Outcome of the dispute */
56
+ outcome: 'challenger-wins' | 'majority-wins' | 'inconclusive';
57
+ /** All votes collected */
58
+ votes: ArbitrationVote[];
59
+ /** Peers receiving reputation adjustments */
60
+ affectedPeers: {
61
+ peerId: string;
62
+ reason: DisputePenaltyReason;
63
+ }[];
64
+ /** Timestamp of resolution */
65
+ timestamp: number;
66
+ };
67
+
68
+ /** Dispute-specific penalty reasons (mapped to PenaltyReason for reputation) */
69
+ export type DisputePenaltyReason = 'false-approval' | 'dispute-lost';
70
+
71
+ /** Dispute protocol message types */
72
+ export type DisputeMessage =
73
+ | { type: 'challenge'; challenge: DisputeChallenge }
74
+ | { type: 'vote'; vote: ArbitrationVote }
75
+ | { type: 'resolution'; resolution: DisputeResolution };
76
+
77
+ /** Dispute status for transaction queries */
78
+ export type DisputeStatus =
79
+ | 'committed-disputed'
80
+ | 'committed-validated'
81
+ | 'committed-invalidated';
82
+
83
+ /** Engine health state */
84
+ export type EngineHealthState = {
85
+ /** Number of disputes lost in the tracking window */
86
+ disputesLost: number;
87
+ /** Timestamps of recent dispute losses */
88
+ recentLosses: number[];
89
+ /** Whether the engine is flagged as unhealthy */
90
+ unhealthy: boolean;
91
+ /** When the unhealthy flag was set */
92
+ unhealthySince?: number;
93
+ };
94
+
95
+ /** Dispute configuration */
96
+ export interface DisputeConfig {
97
+ /** Enable/disable dispute protocol */
98
+ disputeEnabled: boolean;
99
+ /** Timeout for arbitration in milliseconds (default: 60000) */
100
+ disputeArbitrationTimeoutMs: number;
101
+ /** Number of arbitrators to select (default: same as cluster size) */
102
+ arbitratorCount?: number;
103
+ /** Engine health: max disputes lost in window before marking unhealthy */
104
+ engineHealthDisputeThreshold: number;
105
+ /** Engine health: window in ms for counting disputes (default: 600000 = 10 min) */
106
+ engineHealthWindowMs: number;
107
+ }
108
+
109
+ export const DEFAULT_DISPUTE_CONFIG: DisputeConfig = {
110
+ disputeEnabled: false,
111
+ disputeArbitrationTimeoutMs: 60_000,
112
+ engineHealthDisputeThreshold: 3,
113
+ engineHealthWindowMs: 10 * 60 * 1000,
114
+ };
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./cluster/client.js";
2
2
  export * from "./cluster/cluster-repo.js";
3
3
  export * from "./cluster/service.js";
4
+ export * from "./cluster/rebalance-monitor.js";
4
5
  export * from "./protocol-client.js";
5
6
  export * from "./repo/client.js";
6
7
  export * from "./repo/cluster-coordinator.js";
@@ -26,3 +27,5 @@ export * from "./routing/responsibility.js";
26
27
  export * from "./routing/libp2p-known-peers.js";
27
28
  export * from "./network/network-manager-service.js";
28
29
  export * from "./network/get-network-manager.js";
30
+ export * from "./reputation/index.js";
31
+ export * from "./dispute/index.js";
@@ -3,12 +3,28 @@ import { toString as u8ToString } from 'uint8arrays/to-string'
3
3
  import type { ClusterPeers, FindCoordinatorOptions, IKeyNetwork, IPeerNetwork } from "@optimystic/db-core";
4
4
  import { peerIdFromString } from '@libp2p/peer-id'
5
5
  import { multiaddr } from '@multiformats/multiaddr'
6
- import type { FretService } from 'p2p-fret'
6
+ import type { FretService, SerializedTable } from 'p2p-fret'
7
7
  import { hashKey } from 'p2p-fret'
8
- import { createLogger } from './logger.js'
8
+ import { createLogger, verbose } from './logger.js'
9
+ import type { IPeerReputation } from './reputation/types.js'
9
10
 
10
11
  interface WithFretService { services?: { fret?: FretService } }
11
12
 
13
+ export type NetworkMode = 'forming' | 'joining';
14
+
15
+ export interface PersistedNetworkState {
16
+ version: 1;
17
+ networkHighWaterMark: number;
18
+ lastConnectedTimestamp: number;
19
+ consecutiveIsolatedSessions: number;
20
+ fretTable?: SerializedTable;
21
+ }
22
+
23
+ export interface NetworkStatePersistence {
24
+ load(): Promise<PersistedNetworkState | undefined>;
25
+ save(state: PersistedNetworkState): Promise<void>;
26
+ }
27
+
12
28
  /**
13
29
  * Configuration options for self-coordination behavior
14
30
  */
@@ -26,7 +42,7 @@ export interface SelfCoordinationConfig {
26
42
  */
27
43
  export interface SelfCoordinationDecision {
28
44
  allow: boolean;
29
- reason: 'bootstrap-node' | 'partition-detected' | 'suspicious-shrinkage' | 'grace-period-not-elapsed' | 'extended-isolation' | 'disabled';
45
+ reason: 'bootstrap-node' | 'partition-detected' | 'suspicious-shrinkage' | 'grace-period-not-elapsed' | 'extended-isolation' | 'hwm-decay' | 'disabled';
30
46
  warn?: boolean;
31
47
  }
32
48
 
@@ -34,17 +50,25 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
34
50
  private readonly selfCoordinationConfig: Required<SelfCoordinationConfig>;
35
51
  private networkHighWaterMark = 1;
36
52
  private lastConnectedTime = Date.now();
53
+ private consecutiveIsolatedSessions = 0;
54
+ private readonly networkMode: NetworkMode;
55
+ private readonly persistence?: NetworkStatePersistence;
37
56
 
38
57
  constructor(
39
58
  private readonly libp2p: Libp2p,
40
59
  private readonly clusterSize: number = 16,
41
- selfCoordinationConfig?: SelfCoordinationConfig
60
+ selfCoordinationConfig?: SelfCoordinationConfig,
61
+ networkMode?: NetworkMode,
62
+ persistence?: NetworkStatePersistence,
63
+ private readonly reputation?: IPeerReputation
42
64
  ) {
43
65
  this.selfCoordinationConfig = {
44
66
  gracePeriodMs: selfCoordinationConfig?.gracePeriodMs ?? 30_000,
45
67
  shrinkageThreshold: selfCoordinationConfig?.shrinkageThreshold ?? 0.5,
46
68
  allowSelfCoordination: selfCoordinationConfig?.allowSelfCoordination ?? true
47
69
  };
70
+ this.networkMode = networkMode ?? 'forming';
71
+ this.persistence = persistence;
48
72
  this.setupConnectionTracking();
49
73
  }
50
74
 
@@ -72,6 +96,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
72
96
  const connections = this.libp2p.getConnections?.() ?? [];
73
97
  if (connections.length > 0) {
74
98
  this.lastConnectedTime = Date.now();
99
+ this.consecutiveIsolatedSessions = 0;
75
100
  }
76
101
 
77
102
  try {
@@ -90,6 +115,56 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
90
115
  this.log('network-hwm-updated mark=%d (from connections)', this.networkHighWaterMark);
91
116
  }
92
117
  }
118
+
119
+ this.persistState();
120
+ }
121
+
122
+ async initFromPersistedState(): Promise<void> {
123
+ if (!this.persistence) return;
124
+ const state = await this.persistence.load();
125
+ if (!state) return;
126
+
127
+ this.networkHighWaterMark = state.networkHighWaterMark;
128
+ this.lastConnectedTime = state.lastConnectedTimestamp;
129
+ this.consecutiveIsolatedSessions = state.consecutiveIsolatedSessions;
130
+
131
+ if (state.fretTable) {
132
+ try {
133
+ this.getFret().importTable(state.fretTable);
134
+ } catch (err) { this.log('init:fret-import-skipped %o', err); }
135
+ }
136
+
137
+ // If HWM > 1 but FRET table is empty/self-only, increment isolated sessions
138
+ if (state.networkHighWaterMark > 1) {
139
+ const fretEntryCount = state.fretTable?.entries?.length ?? 0;
140
+ if (fretEntryCount <= 1) {
141
+ this.consecutiveIsolatedSessions++;
142
+ this.log('init:isolated-session count=%d hwm=%d', this.consecutiveIsolatedSessions, this.networkHighWaterMark);
143
+ }
144
+ }
145
+ }
146
+
147
+ private canRetryImprove(fretNeighborIds: string[]): boolean {
148
+ if (this.networkMode !== 'forming') return true;
149
+ if (this.networkHighWaterMark > 1) return true;
150
+ const onlySelf = fretNeighborIds.length <= 1
151
+ && (fretNeighborIds.length === 0 || fretNeighborIds[0] === this.libp2p.peerId.toString());
152
+ return !onlySelf;
153
+ }
154
+
155
+ private persistState(): void {
156
+ if (!this.persistence) return;
157
+ const state: PersistedNetworkState = {
158
+ version: 1,
159
+ networkHighWaterMark: this.networkHighWaterMark,
160
+ lastConnectedTimestamp: this.lastConnectedTime,
161
+ consecutiveIsolatedSessions: this.consecutiveIsolatedSessions,
162
+ };
163
+ try {
164
+ const fret = this.getFret();
165
+ state.fretTable = fret.exportTable();
166
+ } catch { /* FRET not available */ }
167
+ void this.persistence.save(state).catch(err => this.log('persist-state-failed %o', err));
93
168
  }
94
169
 
95
170
  /**
@@ -109,6 +184,12 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
109
184
  return { allow: true, reason: 'bootstrap-node' };
110
185
  }
111
186
 
187
+ // Case 1b: Repeated isolation across sessions — decay HWM to allow eventual self-coordination
188
+ if (this.consecutiveIsolatedSessions >= 3) {
189
+ this.log('self-coord-allowed: hwm-decayed sessions=%d', this.consecutiveIsolatedSessions);
190
+ return { allow: true, reason: 'hwm-decay', warn: true };
191
+ }
192
+
112
193
  // Case 2: Check for partition via FRET
113
194
  try {
114
195
  const fret = this.getFret();
@@ -199,6 +280,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
199
280
  }
200
281
 
201
282
  async findCoordinator(key: Uint8Array, _options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
283
+ const t0 = Date.now();
202
284
  const excludedSet = new Set<string>((_options?.excludedPeers ?? []).map(p => p.toString()))
203
285
  const keyStr = this.toCacheKey(key).substring(0, 12);
204
286
 
@@ -207,7 +289,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
207
289
  // honor cache if not excluded
208
290
  const cached = this.getCachedCoordinator(key)
209
291
  if (cached != null && !excludedSet.has(cached.toString())) {
210
- this.log('findCoordinator:cached-hit key=%s coordinator=%s', keyStr, cached.toString().substring(0, 12))
292
+ this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'cache')
211
293
  return cached
212
294
  }
213
295
 
@@ -222,35 +304,49 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
222
304
  this.log('findCoordinator:connected-peers key=%s count=%d peers=%o attempt=%d', keyStr, connected.length, connected.map(p => p.toString().substring(0, 12)), attempt)
223
305
 
224
306
  // prefer FRET neighbors that are also connected, pick first non-excluded
307
+ let ids: string[] = [];
225
308
  try {
226
- const ids = await this.getNeighborIdsForKey(key, this.clusterSize)
227
- this.log('findCoordinator:fret-neighbors key=%s candidates=%o', keyStr, ids.map(s => s.substring(0, 12)))
228
-
229
- // Filter to only connected FRET neighbors
230
- const connectedFretIds = ids.filter(id => connectedSet.has(id) || id === this.libp2p.peerId.toString())
309
+ ids = await this.getNeighborIdsForKey(key, this.clusterSize)
310
+ this.log('findCoordinator:fret-neighbors key=%s candidates=%d', keyStr, ids.length)
311
+ if (verbose) this.log('findCoordinator:fret-candidates key=%s ids=%o connected=%o', keyStr, ids, Array.from(connectedSet))
312
+
313
+ // Filter to only connected FRET neighbors, excluding banned peers
314
+ const connectedFretIds = ids
315
+ .filter(id => (connectedSet.has(id) || id === this.libp2p.peerId.toString())
316
+ && !excludedSet.has(id)
317
+ && !(this.reputation?.isBanned(id)))
318
+ .sort((a, b) => (this.reputation?.getScore(a) ?? 0) - (this.reputation?.getScore(b) ?? 0))
231
319
  this.log('findCoordinator:fret-connected key=%s count=%d peers=%o', keyStr, connectedFretIds.length, connectedFretIds.map(s => s.substring(0, 12)))
232
320
 
233
- const pick = connectedFretIds.find(id => !excludedSet.has(id))
321
+ const pick = connectedFretIds[0]
234
322
  if (pick) {
235
323
  const pid = peerIdFromString(pick)
236
324
  this.recordCoordinator(key, pid)
237
- this.log('findCoordinator:fret-selected key=%s coordinator=%s', keyStr, pick.substring(0, 12))
325
+ this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'fret')
238
326
  return pid
239
327
  }
240
328
  } catch (err) {
241
329
  this.log('findCoordinator getNeighborIdsForKey failed - %o', err)
242
330
  }
243
331
 
244
- // fallback: prefer any existing connected peer that's not excluded
245
- const connectedPick = connected.find(p => !excludedSet.has(p.toString()))
332
+ // fallback: prefer any existing connected peer that's not excluded or banned
333
+ const connectedPick = connected
334
+ .filter(p => !excludedSet.has(p.toString()) && !(this.reputation?.isBanned(p.toString())))
335
+ .sort((a, b) => (this.reputation?.getScore(a.toString()) ?? 0) - (this.reputation?.getScore(b.toString()) ?? 0))
336
+ [0]
246
337
  if (connectedPick) {
247
338
  this.recordCoordinator(key, connectedPick)
248
- this.log('findCoordinator:connected-fallback key=%s coordinator=%s', keyStr, connectedPick.toString().substring(0, 12))
339
+ this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'connected-fallback')
249
340
  return connectedPick
250
341
  }
251
342
 
252
343
  // If no connections and not the last attempt, wait and retry
253
344
  if (connected.length === 0 && attempt < maxRetries - 1) {
345
+ if (!this.canRetryImprove(ids)) {
346
+ this.log('findCoordinator:retry-futile key=%s mode=%s hwm=%d',
347
+ keyStr, this.networkMode, this.networkHighWaterMark);
348
+ break;
349
+ }
254
350
  this.log('findCoordinator:no-connections-retry key=%s attempt=%d delay=%dms', keyStr, attempt, retryDelayMs)
255
351
  await new Promise(resolve => setTimeout(resolve, retryDelayMs))
256
352
  continue
@@ -272,6 +368,7 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
272
368
  this.log('findCoordinator:self-selected key=%s coordinator=%s reason=%s',
273
369
  keyStr, self.toString().substring(0, 12), decision.reason);
274
370
  }
371
+ this.log('findCoordinator:done key=%s ms=%d source=%s', keyStr, Date.now() - t0, 'self')
275
372
  return self
276
373
  }
277
374
 
@@ -299,10 +396,12 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
299
396
  }
300
397
 
301
398
  async findCluster(key: Uint8Array): Promise<ClusterPeers> {
399
+ const t0 = Date.now();
302
400
  const fret = this.getFret()
303
401
  const coord = await hashKey(key)
304
402
  const cohort = fret.assembleCohort(coord, this.clusterSize)
305
403
  const keyStr = this.toCacheKey(key).substring(0, 12);
404
+ this.log('findCluster:start key=%s', keyStr);
306
405
 
307
406
  // Include self in the cohort
308
407
  const ids = Array.from(new Set([...cohort, this.libp2p.peerId.toString()]))
@@ -310,8 +409,8 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
310
409
  const connectedByPeer = this.getConnectedAddrsByPeer()
311
410
  const connectedPeerIds = Object.keys(connectedByPeer)
312
411
 
313
- this.log('findCluster key=%s fretCohort=%d connected=%d cohortPeers=%o',
314
- keyStr, cohort.length, connectedPeerIds.length, ids.map(s => s.substring(0, 12)))
412
+ this.log('findCluster key=%s fretCohort=%d connected=%d', keyStr, cohort.length, connectedPeerIds.length)
413
+ if (verbose) this.log('findCluster:detail key=%s cohortPeers=%o connectedPeers=%o', keyStr, ids, connectedPeerIds)
315
414
 
316
415
  const peers: ClusterPeers = {}
317
416
 
@@ -321,13 +420,12 @@ export class Libp2pKeyPeerNetwork implements IKeyNetwork, IPeerNetwork {
321
420
  continue
322
421
  }
323
422
  const strings = connectedByPeer[idStr] ?? []
324
- peers[idStr] = { multiaddrs: this.parseMultiaddrs(strings), publicKey: new Uint8Array() }
423
+ const remotePeerId = peerIdFromString(idStr)
424
+ peers[idStr] = { multiaddrs: this.parseMultiaddrs(strings), publicKey: remotePeerId.publicKey?.raw ?? new Uint8Array() }
325
425
  }
326
426
 
327
- this.log('findCluster:result key=%s clusterSize=%d withAddrs=%d connectedInCohort=%d',
328
- keyStr, Object.keys(peers).length,
329
- Object.values(peers).filter(p => p.multiaddrs.length > 0).length,
330
- ids.filter(id => connectedPeerIds.includes(id) || id === this.libp2p.peerId.toString()).length)
427
+ this.log('findCluster:done key=%s ms=%d peers=%d',
428
+ keyStr, Date.now() - t0, Object.keys(peers).length)
331
429
  return peers
332
430
  }
333
431
  }
@@ -7,6 +7,7 @@ import { gossipsub } from '@chainsafe/libp2p-gossipsub';
7
7
  import { bootstrap } from '@libp2p/bootstrap';
8
8
  import { circuitRelayServer } from '@libp2p/circuit-relay-v2';
9
9
  import { peerIdFromString } from '@libp2p/peer-id';
10
+ import { generateKeyPair } from '@libp2p/crypto/keys';
10
11
  import { clusterService } from './cluster/service.js';
11
12
  import { repoService } from './repo/service.js';
12
13
  import { StorageRepo } from './storage/storage-repo.js';
@@ -15,7 +16,7 @@ import { MemoryRawStorage } from './storage/memory-storage.js';
15
16
  import type { IRawStorage } from './storage/i-raw-storage.js';
16
17
  import { clusterMember } from './cluster/cluster-repo.js';
17
18
  import { coordinatorRepo } from './repo/coordinator-repo.js';
18
- import { Libp2pKeyPeerNetwork } from './libp2p-key-network.js';
19
+ import { Libp2pKeyPeerNetwork, type NetworkMode, type NetworkStatePersistence } from './libp2p-key-network.js';
19
20
  import { ClusterClient } from './cluster/client.js';
20
21
  import type { IRepo, ICluster, ITransactionValidator } from '@optimystic/db-core';
21
22
  import { networkManagerService } from './network/network-manager-service.js';
@@ -31,6 +32,12 @@ import { ArachnodeFretAdapter } from './storage/arachnode-fret-adapter.js';
31
32
  import type { RestoreCallback } from './storage/struct.js';
32
33
  import type { FretService } from 'p2p-fret';
33
34
  import { PartitionDetector } from './cluster/partition-detector.js';
35
+ import { PeerReputationService } from './reputation/peer-reputation.js';
36
+ import { DisputeService } from './dispute/dispute-service.js';
37
+ import { DisputeClient } from './dispute/client.js';
38
+ import { disputeProtocolService } from './dispute/service.js';
39
+ import { selectArbitrators } from './dispute/arbitrator-selection.js';
40
+ import type { DisputeConfig } from './dispute/types.js';
34
41
 
35
42
  type Libp2pInit = NonNullable<Parameters<typeof createLibp2p>[0]>;
36
43
  export type Libp2pTransports = NonNullable<Libp2pInit['transports']>;
@@ -80,6 +87,12 @@ export type NodeOptions = {
80
87
 
81
88
  /** Transaction validator for cluster consensus */
82
89
  validator?: ITransactionValidator;
90
+
91
+ /** Optional persistence for network state (HWM, FRET table) across restarts */
92
+ persistence?: NetworkStatePersistence;
93
+
94
+ /** Dispute protocol configuration */
95
+ dispute?: Partial<DisputeConfig>;
83
96
  };
84
97
 
85
98
  function resolveStorage(provider: RawStorageProvider | undefined): IRawStorage {
@@ -139,15 +152,15 @@ export async function createLibp2pNodeBase(
139
152
  }
140
153
  };
141
154
 
142
- // Parse peer ID if provided
143
- const peerId = options.id ? await peerIdFromString(options.id) : undefined;
155
+ // Generate or derive the private key for this node
156
+ const nodePrivateKey = await generateKeyPair('Ed25519');
144
157
 
145
158
  const listenAddrs = options.listenAddrs ?? defaults.listenAddrs;
146
159
  const transports = options.transports ?? defaults.transports;
147
160
 
148
161
  const libp2pOptions: unknown = {
149
162
  start: false,
150
- ...(peerId ? { peerId } : {}),
163
+ privateKey: nodePrivateKey,
151
164
  addresses: {
152
165
  listen: listenAddrs
153
166
  },
@@ -251,23 +264,43 @@ export async function createLibp2pNodeBase(
251
264
 
252
265
  await node.start();
253
266
 
267
+ // Initialize peer reputation service
268
+ const reputation = new PeerReputationService();
269
+
254
270
  // Initialize cluster coordination components
255
- const keyNetwork = new Libp2pKeyPeerNetwork(node);
271
+ const networkMode: NetworkMode = (options.bootstrapNodes?.length ?? 0) > 0 ? 'joining' : 'forming';
272
+ const keyNetwork = new Libp2pKeyPeerNetwork(node, options.clusterSize, undefined, networkMode, options.persistence, reputation);
273
+ await keyNetwork.initFromPersistedState();
256
274
  const protocolPrefix = `/optimystic/${options.networkName}`;
257
275
  const createClusterClient = (peerId: any) => ClusterClient.create(peerId, keyNetwork, protocolPrefix);
258
276
 
277
+ // Inject reputation into NetworkManagerService
278
+ try { ((node as any).services?.networkManager as any)?.setReputation?.(reputation); } catch { }
279
+
259
280
  // Create partition detector and get FRET service
260
281
  const partitionDetector = new PartitionDetector();
261
282
  const fretSvc = (node as any).services?.fret as FretService | undefined;
262
283
 
284
+ const consensusConfig = {
285
+ superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? 0.67,
286
+ simpleMajorityThreshold: 0.51,
287
+ minAbsoluteClusterSize: 2,
288
+ allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
289
+ clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
290
+ partitionDetectionWindow: 60000
291
+ };
292
+
263
293
  clusterImpl = clusterMember({
264
294
  storageRepo,
265
295
  peerNetwork: keyNetwork,
266
296
  peerId: node.peerId,
297
+ privateKey: nodePrivateKey,
267
298
  protocolPrefix,
268
299
  partitionDetector,
269
300
  fretService: fretSvc,
270
- validator: options.validator
301
+ validator: options.validator,
302
+ reputation,
303
+ consensusConfig
271
304
  });
272
305
 
273
306
  const coordinatorRepoFactory = coordinatorRepo(
@@ -275,14 +308,10 @@ export async function createLibp2pNodeBase(
275
308
  createClusterClient,
276
309
  {
277
310
  clusterSize: options.clusterSize ?? 10,
278
- superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? 0.67,
279
- simpleMajorityThreshold: 0.51,
280
- minAbsoluteClusterSize: 2,
281
- allowClusterDownsize: options.clusterPolicy?.allowDownsize ?? true,
282
- clusterSizeTolerance: options.clusterPolicy?.sizeTolerance ?? 0.5,
283
- partitionDetectionWindow: 60000
311
+ ...consensusConfig
284
312
  },
285
- fretSvc
313
+ fretSvc,
314
+ reputation
286
315
  );
287
316
 
288
317
  // Create callback for querying cluster peers for their latest block revision
@@ -377,10 +406,45 @@ export async function createLibp2pNodeBase(
377
406
  }
378
407
  }
379
408
 
409
+ // Initialize dispute service if enabled
410
+ let disputeServiceInstance: DisputeService | undefined;
411
+ if (options.dispute?.disputeEnabled) {
412
+ const createDisputeClient = (peerId: any) => DisputeClient.create(peerId, keyNetwork, protocolPrefix);
413
+ disputeServiceInstance = new DisputeService({
414
+ peerId: node.peerId,
415
+ privateKey: nodePrivateKey,
416
+ peerNetwork: keyNetwork,
417
+ createDisputeClient,
418
+ reputation,
419
+ validator: options.validator,
420
+ config: options.dispute,
421
+ selectArbitrators: async (blockId: string, excludePeers: string[], count: number) => {
422
+ const { hashKey: fretHashKey } = await import('p2p-fret');
423
+ const blockIdBytes = new TextEncoder().encode(blockId);
424
+ const fret = (node as any).services?.fret as FretService | undefined;
425
+ if (!fret) return [];
426
+ // Get a larger cohort and exclude the original cluster peers
427
+ const cohortSize = count + excludePeers.length + 1;
428
+ const hashedCoord = await fretHashKey(blockIdBytes);
429
+ const allPeerIdStrs = fret.assembleCohort(hashedCoord, cohortSize) as string[];
430
+ // Filter out original cluster peers and self, convert to PeerId
431
+ const excludeSet = new Set(excludePeers);
432
+ excludeSet.add(node.peerId.toString());
433
+ const arbitratorPeerIds = allPeerIdStrs
434
+ .filter(pid => !excludeSet.has(pid))
435
+ .slice(0, count)
436
+ .map(pid => peerIdFromString(pid));
437
+ return arbitratorPeerIds;
438
+ },
439
+ });
440
+ }
441
+
380
442
  // Expose coordinated repo and storage for external use
381
443
  (node as any).coordinatedRepo = coordinatedRepo;
382
444
  (node as any).storageRepo = storageRepo;
383
445
  (node as any).keyNetwork = keyNetwork;
446
+ (node as any).reputation = reputation;
447
+ (node as any).disputeService = disputeServiceInstance;
384
448
 
385
449
  return node;
386
450
  }
package/src/logger.ts CHANGED
@@ -6,4 +6,5 @@ export function createLogger(subNamespace: string): debug.Debugger {
6
6
  return debug(`${BASE_NAMESPACE}:${subNamespace}`)
7
7
  }
8
8
 
9
-
9
+ export const verbose = typeof process !== 'undefined'
10
+ && (process.env.OPTIMYSTIC_VERBOSE === '1' || process.env.OPTIMYSTIC_VERBOSE === 'true');