@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
|
@@ -3,6 +3,11 @@ import { peerIdFromString } from '@libp2p/peer-id'
|
|
|
3
3
|
import type { FretService } from 'p2p-fret'
|
|
4
4
|
import { hashKey } from 'p2p-fret'
|
|
5
5
|
import { toString as u8ToString } from 'uint8arrays/to-string'
|
|
6
|
+
import type { IPeerReputation } from '../reputation/types.js'
|
|
7
|
+
import { PenaltyReason } from '../reputation/types.js'
|
|
8
|
+
import { RebalanceMonitor, type RebalanceMonitorConfig } from '../cluster/rebalance-monitor.js'
|
|
9
|
+
import type { PartitionDetector } from '../cluster/partition-detector.js'
|
|
10
|
+
import type { ArachnodeFretAdapter } from '../storage/arachnode-fret-adapter.js'
|
|
6
11
|
|
|
7
12
|
export type NetworkManagerServiceInit = {
|
|
8
13
|
clusterSize?: number
|
|
@@ -33,9 +38,9 @@ export class NetworkManagerService implements Startable {
|
|
|
33
38
|
private readonly coordinatorCache = new Map<string, { id: PeerId, expires: number }>()
|
|
34
39
|
private readonly clusterCache = new Map<string, { ids: PeerId[], expires: number }>()
|
|
35
40
|
private lastEstimate: { estimate: number, samples: number, updated: number } | null = null
|
|
36
|
-
|
|
37
|
-
private readonly blacklist = new Map<string, { score: number, expires: number }>()
|
|
41
|
+
private reputation?: IPeerReputation
|
|
38
42
|
private libp2pRef: Libp2p | undefined
|
|
43
|
+
private rebalanceMonitor?: RebalanceMonitor
|
|
39
44
|
|
|
40
45
|
constructor(private readonly components: Components, init: NetworkManagerServiceInit = {}) {
|
|
41
46
|
this.log = components.logger.forComponent('db-p2p:network-manager')
|
|
@@ -55,6 +60,34 @@ export class NetworkManagerService implements Startable {
|
|
|
55
60
|
this.libp2pRef = libp2p;
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
setReputation(reputation: IPeerReputation): void {
|
|
64
|
+
this.reputation = reputation;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the rebalance monitor. Call after libp2p, FRET, and adapter are available.
|
|
69
|
+
*/
|
|
70
|
+
initRebalanceMonitor(
|
|
71
|
+
partitionDetector: PartitionDetector,
|
|
72
|
+
fretAdapter: ArachnodeFretAdapter,
|
|
73
|
+
config?: RebalanceMonitorConfig
|
|
74
|
+
): RebalanceMonitor {
|
|
75
|
+
const libp2p = this.getLibp2p()
|
|
76
|
+
const fret = this.getFret()
|
|
77
|
+
if (!libp2p || !fret) {
|
|
78
|
+
throw new Error('Cannot init RebalanceMonitor: libp2p or FRET not available')
|
|
79
|
+
}
|
|
80
|
+
this.rebalanceMonitor = new RebalanceMonitor(
|
|
81
|
+
{ libp2p, fret, partitionDetector, fretAdapter },
|
|
82
|
+
config
|
|
83
|
+
)
|
|
84
|
+
return this.rebalanceMonitor
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getRebalanceMonitor(): RebalanceMonitor | undefined {
|
|
88
|
+
return this.rebalanceMonitor
|
|
89
|
+
}
|
|
90
|
+
|
|
58
91
|
private getLibp2p(): Libp2p | undefined {
|
|
59
92
|
return this.libp2pRef ?? this.components.libp2p;
|
|
60
93
|
}
|
|
@@ -77,6 +110,9 @@ export class NetworkManagerService implements Startable {
|
|
|
77
110
|
}
|
|
78
111
|
|
|
79
112
|
async stop(): Promise<void> {
|
|
113
|
+
if (this.rebalanceMonitor) {
|
|
114
|
+
await this.rebalanceMonitor.stop()
|
|
115
|
+
}
|
|
80
116
|
this.running = false
|
|
81
117
|
}
|
|
82
118
|
|
|
@@ -162,23 +198,14 @@ export class NetworkManagerService implements Startable {
|
|
|
162
198
|
}
|
|
163
199
|
|
|
164
200
|
/**
|
|
165
|
-
* Record a misbehaving peer.
|
|
166
|
-
* Entries expire to allow eventual forgiveness.
|
|
201
|
+
* Record a misbehaving peer. Delegates to IPeerReputation if available.
|
|
167
202
|
*/
|
|
168
|
-
reportBadPeer(peerId: PeerId,
|
|
169
|
-
|
|
170
|
-
const prev = this.blacklist.get(id)
|
|
171
|
-
const score = (prev?.score ?? 0) + Math.max(1, penalty)
|
|
172
|
-
this.blacklist.set(id, { score, expires: Date.now() + ttlMs })
|
|
203
|
+
reportBadPeer(peerId: PeerId, reason: PenaltyReason = PenaltyReason.ConnectionFailure): void {
|
|
204
|
+
this.reputation?.reportPeer(peerId.toString(), reason)
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
private isBlacklisted(peerId: PeerId): boolean {
|
|
176
|
-
|
|
177
|
-
const rec = this.blacklist.get(id)
|
|
178
|
-
if (!rec) return false
|
|
179
|
-
if (rec.expires <= Date.now()) { this.blacklist.delete(id); return false }
|
|
180
|
-
// simple threshold; can be tuned or exposed later
|
|
181
|
-
return rec.score >= 3
|
|
208
|
+
return this.reputation?.isBanned(peerId.toString()) ?? false
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
recordCoordinator(key: Uint8Array, peerId: PeerId): void {
|
|
@@ -298,7 +325,11 @@ export class NetworkManagerService implements Startable {
|
|
|
298
325
|
if (!libp2p) {
|
|
299
326
|
throw new Error('Libp2p not initialized');
|
|
300
327
|
}
|
|
301
|
-
|
|
328
|
+
// Prefer non-banned, non-deprioritized peers; fall back to deprioritized before self
|
|
329
|
+
const candidate = cluster
|
|
330
|
+
.filter(p => !this.isBlacklisted(p))
|
|
331
|
+
.sort((a, b) => (this.reputation?.getScore(a.toString()) ?? 0) - (this.reputation?.getScore(b.toString()) ?? 0))
|
|
332
|
+
[0] ?? libp2p.peerId;
|
|
302
333
|
this.recordCoordinator(key, candidate);
|
|
303
334
|
return candidate;
|
|
304
335
|
}
|
package/src/protocol-client.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { encode as lpEncode, decode as lpDecode } from 'it-length-prefixed';
|
|
|
3
3
|
import type { Stream as Libp2pStream } from '@libp2p/interface';
|
|
4
4
|
import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
|
|
5
5
|
import { first } from './it-utility.js';
|
|
6
|
+
import { createLogger } from './logger.js';
|
|
7
|
+
|
|
8
|
+
const log = createLogger('protocol-client');
|
|
6
9
|
|
|
7
10
|
/** Base class for clients that communicate via a libp2p protocol */
|
|
8
11
|
export class ProtocolClient {
|
|
@@ -14,13 +17,25 @@ export class ProtocolClient {
|
|
|
14
17
|
protected async processMessage<T>(
|
|
15
18
|
message: unknown,
|
|
16
19
|
protocol: string,
|
|
17
|
-
options?: { signal?: AbortSignal }
|
|
20
|
+
options?: { signal?: AbortSignal; correlationId?: string }
|
|
18
21
|
): Promise<T> {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const peer = this.peerId.toString();
|
|
23
|
+
const cid = options?.correlationId;
|
|
24
|
+
log('dial peer=%s protocol=%s%s', peer, protocol, cid ? ` cid=${cid}` : '');
|
|
25
|
+
const t0 = Date.now();
|
|
26
|
+
|
|
27
|
+
let stream: Libp2pStream;
|
|
28
|
+
try {
|
|
29
|
+
stream = await this.peerNetwork.connect(
|
|
30
|
+
this.peerId,
|
|
31
|
+
protocol,
|
|
32
|
+
{ signal: options?.signal }
|
|
33
|
+
) as unknown as Libp2pStream;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
log('dial:fail peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
log('dial:ok peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
|
|
24
39
|
|
|
25
40
|
try {
|
|
26
41
|
// Send the request using length-prefixed encoding
|
|
@@ -33,11 +48,16 @@ export class ProtocolClient {
|
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
// Read the response from the stream (which is now directly AsyncIterable)
|
|
51
|
+
let firstByte = true;
|
|
36
52
|
const source = pipe(
|
|
37
53
|
stream,
|
|
38
54
|
lpDecode,
|
|
39
55
|
async function* (source) {
|
|
40
56
|
for await (const data of source) {
|
|
57
|
+
if (firstByte) {
|
|
58
|
+
log('first-byte peer=%s ms=%d%s', peer, Date.now() - t0, cid ? ` cid=${cid}` : '');
|
|
59
|
+
firstByte = false;
|
|
60
|
+
}
|
|
41
61
|
const decoded = new TextDecoder().decode(data.subarray());
|
|
42
62
|
const parsed = JSON.parse(decoded);
|
|
43
63
|
yield parsed;
|
|
@@ -45,7 +65,9 @@ export class ProtocolClient {
|
|
|
45
65
|
}
|
|
46
66
|
) as AsyncIterable<T>;
|
|
47
67
|
|
|
48
|
-
|
|
68
|
+
const result = await first(() => source, () => { throw new Error('No response received') });
|
|
69
|
+
log('response peer=%s protocol=%s ms=%d%s', peer, protocol, Date.now() - t0, cid ? ` cid=${cid}` : '');
|
|
70
|
+
return result;
|
|
49
71
|
} finally {
|
|
50
72
|
await stream.close();
|
|
51
73
|
}
|
package/src/repo/client.ts
CHANGED
|
@@ -44,6 +44,15 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
private extractCorrelationId(operations: RepoMessage['operations']): string | undefined {
|
|
48
|
+
const op = operations[0];
|
|
49
|
+
if (!op) return undefined;
|
|
50
|
+
if ('pend' in op) return op.pend.actionId;
|
|
51
|
+
if ('commit' in op) return op.commit.actionId;
|
|
52
|
+
if ('cancel' in op) return op.cancel.actionRef.actionId;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
private async processRepoMessage<T>(
|
|
48
57
|
operations: RepoMessage['operations'],
|
|
49
58
|
options: MessageOptions,
|
|
@@ -53,6 +62,7 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
53
62
|
operations,
|
|
54
63
|
expiration: options.expiration,
|
|
55
64
|
};
|
|
65
|
+
const correlationId = this.extractCorrelationId(operations);
|
|
56
66
|
const deadline = options.expiration ?? (Date.now() + 30_000)
|
|
57
67
|
const msLeft = Math.max(1, deadline - Date.now())
|
|
58
68
|
const withTimeout = async <U>(fn: () => Promise<U>): Promise<U> => {
|
|
@@ -63,7 +73,7 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
63
73
|
}
|
|
64
74
|
let response: any
|
|
65
75
|
const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
|
|
66
|
-
response = await withTimeout(() => super.processMessage<any>(message, preferred, { signal: options?.signal }))
|
|
76
|
+
response = await withTimeout(() => super.processMessage<any>(message, preferred, { signal: options?.signal, correlationId }))
|
|
67
77
|
|
|
68
78
|
if (response?.redirect?.peers?.length) {
|
|
69
79
|
if (hop >= 2) {
|
|
@@ -75,11 +85,11 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
75
85
|
if (next.id === currentIdStr) {
|
|
76
86
|
throw new Error('Redirect loop detected in RepoClient (same peer)')
|
|
77
87
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
// cache hint
|
|
89
|
+
this.recordCoordinatorForOpsIfSupported(operations, nextId)
|
|
90
|
+
// single-hop retry against target peer using repo protocol
|
|
91
|
+
const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
|
|
92
|
+
return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
|
|
83
93
|
}
|
|
84
94
|
return response as T;
|
|
85
95
|
}
|
|
@@ -97,6 +107,10 @@ export class RepoClient extends ProtocolClient implements IRepo {
|
|
|
97
107
|
if ('commit' in op) {
|
|
98
108
|
return new TextEncoder().encode(op.commit.tailId);
|
|
99
109
|
}
|
|
110
|
+
if ('cancel' in op) {
|
|
111
|
+
const id = op.cancel.actionRef.blockIds[0];
|
|
112
|
+
return id ? new TextEncoder().encode(id) : undefined;
|
|
113
|
+
}
|
|
100
114
|
return undefined;
|
|
101
115
|
}
|
|
102
116
|
|
|
@@ -5,9 +5,11 @@ import { sha256 } from "multiformats/hashes/sha2";
|
|
|
5
5
|
import { ClusterClient } from "../cluster/client.js";
|
|
6
6
|
import { Pending } from "@optimystic/db-core";
|
|
7
7
|
import type { PeerId } from "@libp2p/interface";
|
|
8
|
-
import { createLogger } from '../logger.js'
|
|
8
|
+
import { createLogger, verbose } from '../logger.js'
|
|
9
9
|
import type { ClusterLogPeerOutcome } from './types.js'
|
|
10
10
|
import type { FretService } from "p2p-fret";
|
|
11
|
+
import type { IPeerReputation } from "../reputation/types.js";
|
|
12
|
+
import { PenaltyReason } from "../reputation/types.js";
|
|
11
13
|
|
|
12
14
|
const log = createLogger('cluster')
|
|
13
15
|
|
|
@@ -49,7 +51,8 @@ export class ClusterCoordinator {
|
|
|
49
51
|
peerId: PeerId;
|
|
50
52
|
wasTransactionExecuted?: (messageHash: string) => boolean;
|
|
51
53
|
},
|
|
52
|
-
private readonly fretService?: FretService
|
|
54
|
+
private readonly fretService?: FretService,
|
|
55
|
+
private readonly reputation?: IPeerReputation
|
|
53
56
|
) { }
|
|
54
57
|
|
|
55
58
|
/**
|
|
@@ -248,6 +251,28 @@ export class ClusterCoordinator {
|
|
|
248
251
|
throw new Error(`Failed to get super-majority: ${approvalCount}/${peerCount} approvals (needed ${superMajority}, ${rejectionCount} rejections)`);
|
|
249
252
|
}
|
|
250
253
|
|
|
254
|
+
// Mark as disputed when minority rejections exist but super-majority approves
|
|
255
|
+
if (rejectionCount > 0 && approvalCount >= superMajority) {
|
|
256
|
+
const rejectingPeers: string[] = [];
|
|
257
|
+
const rejectReasons: { [peerId: string]: string } = {};
|
|
258
|
+
for (const [peerId, sig] of Object.entries(promises)) {
|
|
259
|
+
if (sig.type === 'reject') {
|
|
260
|
+
rejectingPeers.push(peerId);
|
|
261
|
+
rejectReasons[peerId] = sig.rejectReason ?? 'unknown';
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
promised.record.disputed = true;
|
|
265
|
+
promised.record.disputeEvidence = { rejectingPeers, rejectReasons };
|
|
266
|
+
log('cluster-tx:disputed', {
|
|
267
|
+
messageHash: record.messageHash,
|
|
268
|
+
rejectingPeers,
|
|
269
|
+
rejectReasons,
|
|
270
|
+
approvalCount,
|
|
271
|
+
rejectionCount,
|
|
272
|
+
peerCount
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
251
276
|
return await this.commitTransaction(promised.record);
|
|
252
277
|
}
|
|
253
278
|
|
|
@@ -301,6 +326,13 @@ export class ClusterCoordinator {
|
|
|
301
326
|
private async collectPromises(peers: ClusterPeers, record: ClusterRecord): Promise<{ record: ClusterRecord }> {
|
|
302
327
|
const peerIds = Object.keys(peers);
|
|
303
328
|
const summary: ClusterLogPeerOutcome[] = [];
|
|
329
|
+
if (verbose) {
|
|
330
|
+
const peerDetail = peerIds.map(id => ({
|
|
331
|
+
id: id.substring(0, 12),
|
|
332
|
+
addrs: peers[id]?.multiaddrs?.length ?? 0
|
|
333
|
+
}));
|
|
334
|
+
log('cluster-tx:promise-peers', { messageHash: record.messageHash, peers: peerDetail });
|
|
335
|
+
}
|
|
304
336
|
// For each peer, create a client and request a promise
|
|
305
337
|
const promiseRequests = peerIds.map(peerIdStr => {
|
|
306
338
|
const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
|
|
@@ -327,6 +359,7 @@ export class ClusterCoordinator {
|
|
|
327
359
|
const peerIdStr = peerIds[idx]!;
|
|
328
360
|
log('cluster-tx:promise-response', { messageHash: record.messageHash, peerId: peerIdStr, success: false, error: err });
|
|
329
361
|
summary.push({ peerId: peerIdStr, success: false, error: err instanceof Error ? err.message : String(err) });
|
|
362
|
+
this.reputation?.reportPeer(peerIdStr, PenaltyReason.ConsensusTimeout, `promise:${record.messageHash}`);
|
|
330
363
|
return null;
|
|
331
364
|
})));
|
|
332
365
|
const successes = summary.filter(entry => entry.success).map(entry => entry.peerId);
|
|
@@ -390,6 +423,13 @@ export class ClusterCoordinator {
|
|
|
390
423
|
// For each peer, create a client and send the commit
|
|
391
424
|
const peerIds = Object.keys(record.peers);
|
|
392
425
|
const summary: ClusterLogPeerOutcome[] = [];
|
|
426
|
+
if (verbose) {
|
|
427
|
+
const peerDetail = peerIds.map(id => ({
|
|
428
|
+
id: id.substring(0, 12),
|
|
429
|
+
addrs: record.peers[id]?.multiaddrs?.length ?? 0
|
|
430
|
+
}));
|
|
431
|
+
log('cluster-tx:commit-peers', { messageHash: record.messageHash, peers: peerDetail });
|
|
432
|
+
}
|
|
393
433
|
// Send the record with promises to all peers
|
|
394
434
|
// Each peer will add its own commit signature
|
|
395
435
|
const commitPayload = {
|
|
@@ -414,6 +454,7 @@ export class ClusterCoordinator {
|
|
|
414
454
|
const peerIdStr = peerIds[idx]!;
|
|
415
455
|
log('cluster-tx:commit-response', { messageHash: record.messageHash, peerId: peerIdStr, success: false, error: err });
|
|
416
456
|
summary.push({ peerId: peerIdStr, success: false, error: err instanceof Error ? err.message : String(err) });
|
|
457
|
+
this.reputation?.reportPeer(peerIdStr, PenaltyReason.ConsensusTimeout, `commit:${record.messageHash}`);
|
|
417
458
|
return null;
|
|
418
459
|
})));
|
|
419
460
|
const commitSuccesses = summary.filter(entry => entry.success).map(entry => entry.peerId);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { PendRequest, ActionBlocks, IRepo, MessageOptions, CommitResult, GetBlockResults, PendResult, BlockGets, CommitRequest, RepoMessage, IKeyNetwork, ICluster, ClusterConsensusConfig, BlockId, ActionRev } from "@optimystic/db-core";
|
|
2
|
+
import { LruMap } from "@optimystic/db-core";
|
|
2
3
|
import { ClusterCoordinator } from "./cluster-coordinator.js";
|
|
3
4
|
import type { ClusterClient } from "../cluster/client.js";
|
|
4
5
|
import type { PeerId } from "@libp2p/interface";
|
|
5
6
|
import { peerIdFromString } from "@libp2p/peer-id";
|
|
6
7
|
import type { FretService } from "p2p-fret";
|
|
7
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import type { IPeerReputation } from "../reputation/types.js";
|
|
8
10
|
|
|
9
11
|
const log = createLogger('coordinator-repo');
|
|
10
12
|
|
|
@@ -37,7 +39,8 @@ export function coordinatorRepo(
|
|
|
37
39
|
keyNetwork: IKeyNetwork,
|
|
38
40
|
createClusterClient: (peerId: PeerId) => ClusterClient,
|
|
39
41
|
cfg?: Partial<ClusterConsensusConfig> & { clusterSize?: number },
|
|
40
|
-
fretService?: FretService
|
|
42
|
+
fretService?: FretService,
|
|
43
|
+
reputation?: IPeerReputation
|
|
41
44
|
): (components: CoordinatorRepoComponents) => CoordinatorRepo {
|
|
42
45
|
return (components: CoordinatorRepoComponents) => new CoordinatorRepo(
|
|
43
46
|
keyNetwork,
|
|
@@ -47,7 +50,8 @@ export function coordinatorRepo(
|
|
|
47
50
|
components.localCluster,
|
|
48
51
|
components.localPeerId,
|
|
49
52
|
fretService,
|
|
50
|
-
components.clusterLatestCallback
|
|
53
|
+
components.clusterLatestCallback,
|
|
54
|
+
reputation
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -55,6 +59,9 @@ export function coordinatorRepo(
|
|
|
55
59
|
export class CoordinatorRepo implements IRepo {
|
|
56
60
|
private coordinator: ClusterCoordinator;
|
|
57
61
|
private readonly DEFAULT_TIMEOUT = 30000; // 30 seconds default timeout
|
|
62
|
+
private readonly localPeerId?: PeerId;
|
|
63
|
+
private readonly responsibilityCache = new LruMap<string, { inCluster: boolean, expires: number }>(1000);
|
|
64
|
+
private static readonly RESPONSIBILITY_TTL_MS = 60_000;
|
|
58
65
|
|
|
59
66
|
constructor(
|
|
60
67
|
readonly keyNetwork: IKeyNetwork,
|
|
@@ -64,8 +71,10 @@ export class CoordinatorRepo implements IRepo {
|
|
|
64
71
|
localCluster?: LocalClusterWithExecutionTracking,
|
|
65
72
|
localPeerId?: PeerId,
|
|
66
73
|
fretService?: FretService,
|
|
67
|
-
private readonly clusterLatestCallback?: ClusterLatestCallback
|
|
74
|
+
private readonly clusterLatestCallback?: ClusterLatestCallback,
|
|
75
|
+
reputation?: IPeerReputation
|
|
68
76
|
) {
|
|
77
|
+
this.localPeerId = localPeerId;
|
|
69
78
|
const policy: ClusterConsensusConfig & { clusterSize: number } = {
|
|
70
79
|
clusterSize: cfg?.clusterSize ?? 10,
|
|
71
80
|
superMajorityThreshold: cfg?.superMajorityThreshold ?? 0.75,
|
|
@@ -80,10 +89,64 @@ export class CoordinatorRepo implements IRepo {
|
|
|
80
89
|
peerId: localPeerId,
|
|
81
90
|
wasTransactionExecuted: localCluster.wasTransactionExecuted?.bind(localCluster)
|
|
82
91
|
} : undefined;
|
|
83
|
-
this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService);
|
|
92
|
+
this.coordinator = new ClusterCoordinator(keyNetwork, createClusterClient, policy, localClusterRef, fretService, reputation);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if this node is in the cluster for a given block.
|
|
97
|
+
* Uses findCluster membership — in the real network layer, self is always
|
|
98
|
+
* included in the cohort when this node is responsible. This serves as a
|
|
99
|
+
* defense-in-depth guard for requests that arrive at the wrong node.
|
|
100
|
+
* Returns true if localPeerId is not set (backward compat for single-node/test setups).
|
|
101
|
+
*/
|
|
102
|
+
private async isResponsibleForBlock(blockId: BlockId): Promise<boolean> {
|
|
103
|
+
if (!this.localPeerId) return true;
|
|
104
|
+
|
|
105
|
+
const cached = this.responsibilityCache.get(blockId);
|
|
106
|
+
if (cached && cached.expires > Date.now()) {
|
|
107
|
+
return cached.inCluster;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const blockIdBytes = new TextEncoder().encode(blockId);
|
|
111
|
+
let inCluster: boolean;
|
|
112
|
+
try {
|
|
113
|
+
const peers = await this.keyNetwork.findCluster(blockIdBytes);
|
|
114
|
+
inCluster = this.localPeerId.toString() in peers;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log('proximity:check-error', { blockId, error: (err as Error).message });
|
|
117
|
+
// On failure, assume responsible to avoid false rejections
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.responsibilityCache.set(blockId, { inCluster, expires: Date.now() + CoordinatorRepo.RESPONSIBILITY_TTL_MS });
|
|
122
|
+
log('proximity:checked', { blockId, inCluster });
|
|
123
|
+
return inCluster;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Verify this node is responsible for all given block IDs. Throws if not.
|
|
128
|
+
*/
|
|
129
|
+
private async verifyResponsibility(blockIds: BlockId[]): Promise<void> {
|
|
130
|
+
const notResponsible: BlockId[] = [];
|
|
131
|
+
for (const blockId of blockIds) {
|
|
132
|
+
if (!await this.isResponsibleForBlock(blockId)) {
|
|
133
|
+
notResponsible.push(blockId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (notResponsible.length > 0) {
|
|
137
|
+
log('proximity:rejected', { blockIds: notResponsible });
|
|
138
|
+
throw new Error(`Not responsible for block(s): ${notResponsible.join(', ')}`);
|
|
139
|
+
}
|
|
84
140
|
}
|
|
85
141
|
|
|
86
142
|
async get(blockGets: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
|
|
143
|
+
// Soft proximity check — warn but still serve reads for graceful degradation
|
|
144
|
+
for (const blockId of blockGets.blockIds) {
|
|
145
|
+
if (!await this.isResponsibleForBlock(blockId)) {
|
|
146
|
+
log('proximity:get-warning', { blockId, msg: 'serving read for non-responsible block' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
87
150
|
// First try local storage
|
|
88
151
|
const localResult = await this.storageRepo.get(blockGets, options);
|
|
89
152
|
|
|
@@ -166,6 +229,7 @@ export class CoordinatorRepo implements IRepo {
|
|
|
166
229
|
|
|
167
230
|
async pend(request: PendRequest, options?: MessageOptions): Promise<PendResult> {
|
|
168
231
|
const allBlockIds = Object.keys(request.transforms);
|
|
232
|
+
await this.verifyResponsibility(allBlockIds);
|
|
169
233
|
const coordinatingBlockIds = (options as any)?.coordinatingBlockIds ?? allBlockIds;
|
|
170
234
|
|
|
171
235
|
const peerCount = await this.coordinator.getClusterSize(coordinatingBlockIds[0]!);
|
|
@@ -209,10 +273,8 @@ export class CoordinatorRepo implements IRepo {
|
|
|
209
273
|
}
|
|
210
274
|
|
|
211
275
|
async cancel(actionRef: ActionBlocks, options?: MessageOptions): Promise<void> {
|
|
212
|
-
// TODO: Verify that we are a proximate node for all block IDs in the request
|
|
213
|
-
|
|
214
|
-
// Extract all block IDs affected by this cancel operation
|
|
215
276
|
const blockIds = actionRef.blockIds;
|
|
277
|
+
await this.verifyResponsibility(blockIds);
|
|
216
278
|
|
|
217
279
|
// Create a message for this cancel operation with timeout
|
|
218
280
|
const message: RepoMessage = {
|
|
@@ -242,6 +304,7 @@ export class CoordinatorRepo implements IRepo {
|
|
|
242
304
|
|
|
243
305
|
async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
|
|
244
306
|
const blockIds = request.blockIds;
|
|
307
|
+
await this.verifyResponsibility(blockIds);
|
|
245
308
|
|
|
246
309
|
const peerCount = await this.coordinator.getClusterSize(blockIds[0]!);
|
|
247
310
|
if (peerCount <= 1) {
|