@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.
- package/dist/src/cluster/block-transfer-service.d.ts +66 -0
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -0
- package/dist/src/cluster/block-transfer-service.js +163 -0
- package/dist/src/cluster/block-transfer-service.js.map +1 -0
- package/dist/src/cluster/block-transfer.d.ts +79 -0
- package/dist/src/cluster/block-transfer.d.ts.map +1 -0
- package/dist/src/cluster/block-transfer.js +211 -0
- package/dist/src/cluster/block-transfer.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +14 -3
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +80 -35
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +64 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -0
- package/dist/src/cluster/rebalance-monitor.js +159 -0
- package/dist/src/cluster/rebalance-monitor.js.map +1 -0
- package/dist/src/cluster/service.js +1 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +10 -0
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -0
- package/dist/src/dispute/arbitrator-selection.js +22 -0
- package/dist/src/dispute/arbitrator-selection.js.map +1 -0
- package/dist/src/dispute/client.d.ts +17 -0
- package/dist/src/dispute/client.d.ts.map +1 -0
- package/dist/src/dispute/client.js +28 -0
- package/dist/src/dispute/client.js.map +1 -0
- package/dist/src/dispute/dispute-service.d.ts +81 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -0
- package/dist/src/dispute/dispute-service.js +365 -0
- package/dist/src/dispute/dispute-service.js.map +1 -0
- package/dist/src/dispute/engine-health-monitor.d.ts +22 -0
- package/dist/src/dispute/engine-health-monitor.d.ts.map +1 -0
- package/dist/src/dispute/engine-health-monitor.js +75 -0
- package/dist/src/dispute/engine-health-monitor.js.map +1 -0
- package/dist/src/dispute/index.d.ts +7 -0
- package/dist/src/dispute/index.d.ts.map +1 -0
- package/dist/src/dispute/index.js +7 -0
- package/dist/src/dispute/index.js.map +1 -0
- package/dist/src/dispute/service.d.ts +41 -0
- package/dist/src/dispute/service.d.ts.map +1 -0
- package/dist/src/dispute/service.js +82 -0
- package/dist/src/dispute/service.js.map +1 -0
- package/dist/src/dispute/types.d.ts +106 -0
- package/dist/src/dispute/types.d.ts.map +1 -0
- package/dist/src/dispute/types.js +7 -0
- package/dist/src/dispute/types.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +23 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +100 -15
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +6 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +66 -12
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +2 -0
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +15 -4
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +33 -20
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/protocol-client.d.ts +1 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +23 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/repo/client.d.ts +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +18 -1
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +3 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +42 -2
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +18 -2
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +62 -6
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +18 -2
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +88 -91
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/index.d.ts +3 -0
- package/dist/src/reputation/index.d.ts.map +1 -0
- package/dist/src/reputation/index.js +3 -0
- package/dist/src/reputation/index.js.map +1 -0
- package/dist/src/reputation/peer-reputation.d.ts +23 -0
- package/dist/src/reputation/peer-reputation.d.ts.map +1 -0
- package/dist/src/reputation/peer-reputation.js +121 -0
- package/dist/src/reputation/peer-reputation.js.map +1 -0
- package/dist/src/reputation/types.d.ts +89 -0
- package/dist/src/reputation/types.d.ts.map +1 -0
- package/dist/src/reputation/types.js +42 -0
- package/dist/src/reputation/types.js.map +1 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts +5 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +10 -0
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +5 -0
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +8 -0
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/package.json +11 -10
- package/src/cluster/block-transfer-service.ts +228 -0
- package/src/cluster/block-transfer.ts +284 -0
- package/src/cluster/cluster-repo.ts +93 -38
- package/src/cluster/rebalance-monitor.ts +225 -0
- package/src/dispute/arbitrator-selection.ts +28 -0
- package/src/dispute/client.ts +41 -0
- package/src/dispute/dispute-service.ts +453 -0
- package/src/dispute/engine-health-monitor.ts +86 -0
- package/src/dispute/index.ts +17 -0
- package/src/dispute/service.ts +119 -0
- package/src/dispute/types.ts +114 -0
- package/src/index.ts +3 -0
- package/src/libp2p-key-network.ts +120 -22
- package/src/libp2p-node-base.ts +77 -13
- package/src/logger.ts +2 -1
- package/src/network/network-manager-service.ts +47 -16
- package/src/protocol-client.ts +29 -7
- package/src/repo/client.ts +20 -6
- package/src/repo/cluster-coordinator.ts +43 -2
- package/src/repo/coordinator-repo.ts +70 -7
- package/src/repo/redirect.ts +0 -2
- package/src/repo/service.ts +95 -87
- package/src/reputation/index.ts +12 -0
- package/src/reputation/peer-reputation.ts +147 -0
- package/src/reputation/types.ts +117 -0
- package/src/storage/arachnode-fret-adapter.ts +11 -0
- package/src/storage/block-storage.ts +6 -0
- package/src/storage/storage-repo.ts +9 -0
- package/dist/index.min.js +0 -53
- 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:
|
|
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
|
-
|
|
227
|
-
this.log('findCoordinator:fret-neighbors key=%s candidates=%
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
314
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/src/libp2p-node-base.ts
CHANGED
|
@@ -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
|
-
//
|
|
143
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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');
|