@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
package/src/repo/service.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe'
|
|
2
2
|
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
|
|
3
|
-
import type { Startable, Logger, Stream, Connection, StreamHandler } from '@libp2p/interface'
|
|
3
|
+
import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } from '@libp2p/interface'
|
|
4
4
|
import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
5
5
|
import { peersEqual } from '../peer-utils.js'
|
|
6
6
|
import { sha256 } from 'multiformats/hashes/sha2'
|
|
7
|
-
import { encodePeers } from './redirect.js'
|
|
7
|
+
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
8
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
9
|
+
import { createLogger } from '../logger.js'
|
|
10
|
+
|
|
11
|
+
const debugLog = createLogger('repo-service')
|
|
9
12
|
|
|
10
13
|
// Define Components interface
|
|
11
14
|
interface BaseComponents {
|
|
@@ -16,8 +19,15 @@ interface BaseComponents {
|
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
export interface NetworkManagerLike {
|
|
23
|
+
getCluster(key: Uint8Array): Promise<PeerId[]>
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export type RepoServiceComponents = BaseComponents & {
|
|
20
27
|
repo: IRepo
|
|
28
|
+
networkManager?: NetworkManagerLike
|
|
29
|
+
peerId?: PeerId
|
|
30
|
+
getConnectionAddrs?: (peerId: PeerId) => string[]
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export type RepoServiceInit = {
|
|
@@ -100,6 +110,61 @@ export class RepoService implements Startable {
|
|
|
100
110
|
this.running = false
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
private getNetworkManager(): NetworkManagerLike | undefined {
|
|
114
|
+
if (this.components.networkManager) return this.components.networkManager
|
|
115
|
+
return (this.components as any).libp2p?.services?.networkManager as NetworkManagerLike | undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getSelfId(): PeerId | undefined {
|
|
119
|
+
if (this.components.peerId) return this.components.peerId
|
|
120
|
+
return (this.components as any).libp2p?.peerId as PeerId | undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private getPeerAddrs(peerId: PeerId): string[] {
|
|
124
|
+
if (this.components.getConnectionAddrs) return this.components.getConnectionAddrs(peerId)
|
|
125
|
+
const libp2p = (this.components as any).libp2p
|
|
126
|
+
if (!libp2p?.getConnections) return []
|
|
127
|
+
const conns: any[] = libp2p.getConnections(peerId) ?? []
|
|
128
|
+
const addrs: string[] = []
|
|
129
|
+
for (const c of conns) {
|
|
130
|
+
const addr = c.remoteAddr?.toString?.()
|
|
131
|
+
if (addr) addrs.push(addr)
|
|
132
|
+
}
|
|
133
|
+
return addrs
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if this node should redirect the request for a given key.
|
|
138
|
+
* Returns a RedirectPayload if not responsible, null if should handle locally.
|
|
139
|
+
* Also attaches cluster info to the message for downstream use.
|
|
140
|
+
*/
|
|
141
|
+
async checkRedirect(blockKey: string, opName: string, message: RepoMessage): Promise<RedirectPayload | null> {
|
|
142
|
+
const nm = this.getNetworkManager()
|
|
143
|
+
if (!nm) return null
|
|
144
|
+
|
|
145
|
+
const mh = await sha256.digest(new TextEncoder().encode(blockKey))
|
|
146
|
+
const key = mh.digest
|
|
147
|
+
const cluster = await nm.getCluster(key)
|
|
148
|
+
;(message as any).cluster = cluster.map((p: PeerId) => p.toString?.() ?? String(p))
|
|
149
|
+
|
|
150
|
+
const selfId = this.getSelfId()
|
|
151
|
+
if (!selfId) return null
|
|
152
|
+
|
|
153
|
+
const isMember = cluster.some((p: PeerId) => peersEqual(p, selfId))
|
|
154
|
+
const smallMesh = cluster.length < this.responsibilityK
|
|
155
|
+
|
|
156
|
+
if (!smallMesh && !isMember) {
|
|
157
|
+
const peers = cluster.filter((p: PeerId) => !peersEqual(p, selfId))
|
|
158
|
+
debugLog('redirect op=%s blockKey=%s cluster=%d', opName, blockKey, cluster.length)
|
|
159
|
+
return encodePeers(peers.map((pid: PeerId) => ({
|
|
160
|
+
id: pid.toString(),
|
|
161
|
+
addrs: this.getPeerAddrs(pid)
|
|
162
|
+
})))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
103
168
|
/**
|
|
104
169
|
* Handle incoming streams on the repo protocol
|
|
105
170
|
*/
|
|
@@ -117,97 +182,40 @@ export class RepoService implements Startable {
|
|
|
117
182
|
let response: any
|
|
118
183
|
|
|
119
184
|
if ('get' in operation) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const cluster: any[] = await nm.getCluster(key);
|
|
127
|
-
(message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
|
|
128
|
-
const selfId = (this.components as any).libp2p.peerId
|
|
129
|
-
const isMember = cluster.some((p: any) => peersEqual(p, selfId))
|
|
130
|
-
// Use responsibilityK to determine if we're in the responsible set
|
|
131
|
-
const smallMesh = cluster.length < this.responsibilityK
|
|
132
|
-
if (!smallMesh && !isMember) {
|
|
133
|
-
const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
|
|
134
|
-
console.debug('repo-service:redirect', {
|
|
135
|
-
peerId: selfId.toString(),
|
|
136
|
-
reason: 'not-cluster-member',
|
|
137
|
-
operation: 'get',
|
|
138
|
-
blockId: operation.get.blockIds[0],
|
|
139
|
-
cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
|
|
140
|
-
})
|
|
141
|
-
response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
|
|
142
|
-
} else {
|
|
143
|
-
response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
|
|
144
|
-
}
|
|
145
|
-
} else {
|
|
146
|
-
response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
|
|
147
|
-
}
|
|
185
|
+
const blockKey = operation.get.blockIds[0]!
|
|
186
|
+
const redirect = await this.checkRedirect(blockKey, 'get', message)
|
|
187
|
+
if (redirect) {
|
|
188
|
+
response = redirect
|
|
189
|
+
} else {
|
|
190
|
+
response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
|
|
148
191
|
}
|
|
149
192
|
} else if ('pend' in operation) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const cluster: any[] = await nm.getCluster(key)
|
|
157
|
-
; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
|
|
158
|
-
const selfId = (this.components as any).libp2p.peerId
|
|
159
|
-
const isMember = cluster.some((p: any) => peersEqual(p, selfId))
|
|
160
|
-
// Use responsibilityK to determine if we're in the responsible set
|
|
161
|
-
const smallMesh = cluster.length < this.responsibilityK
|
|
162
|
-
if (!smallMesh && !isMember) {
|
|
163
|
-
const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
|
|
164
|
-
console.debug('repo-service:redirect', {
|
|
165
|
-
peerId: selfId.toString(),
|
|
166
|
-
reason: 'not-cluster-member',
|
|
167
|
-
operation: 'pend',
|
|
168
|
-
blockId: id,
|
|
169
|
-
cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
|
|
170
|
-
})
|
|
171
|
-
response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
|
|
172
|
-
} else {
|
|
173
|
-
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
177
|
-
}
|
|
193
|
+
const blockKey = Object.keys(operation.pend.transforms)[0]!
|
|
194
|
+
const redirect = await this.checkRedirect(blockKey, 'pend', message)
|
|
195
|
+
if (redirect) {
|
|
196
|
+
response = redirect
|
|
197
|
+
} else {
|
|
198
|
+
response = await this.repo.pend(operation.pend, { expiration: message.expiration })
|
|
178
199
|
}
|
|
179
200
|
} else if ('cancel' in operation) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const mh = await sha256.digest(new TextEncoder().encode(operation.commit.tailId))
|
|
186
|
-
const key = mh.digest
|
|
187
|
-
const nm: any = (this.components as any).libp2p?.services?.networkManager
|
|
188
|
-
if (nm?.getCluster) {
|
|
189
|
-
const cluster: any[] = await nm.getCluster(key)
|
|
190
|
-
; (message as any).cluster = (cluster as any[]).map(p => p.toString?.() ?? String(p))
|
|
191
|
-
const selfId = (this.components as any).libp2p.peerId
|
|
192
|
-
const isMember = cluster.some((p: any) => peersEqual(p, selfId))
|
|
193
|
-
// Use responsibilityK to determine if we're in the responsible set
|
|
194
|
-
const smallMesh = cluster.length < this.responsibilityK
|
|
195
|
-
if (!smallMesh && !isMember) {
|
|
196
|
-
const peers = cluster.filter((p: any) => !peersEqual(p, selfId))
|
|
197
|
-
console.debug('repo-service:redirect', {
|
|
198
|
-
peerId: selfId.toString(),
|
|
199
|
-
reason: 'not-cluster-member',
|
|
200
|
-
operation: 'commit',
|
|
201
|
-
tailId: operation.commit.tailId,
|
|
202
|
-
cluster: cluster.map((p: any) => p.toString?.() ?? String(p))
|
|
203
|
-
})
|
|
204
|
-
response = encodePeers(peers.map((pid: any) => ({ id: pid.toString(), addrs: [] })))
|
|
205
|
-
} else {
|
|
206
|
-
response = await this.repo.commit(operation.commit, { expiration: message.expiration })
|
|
207
|
-
}
|
|
201
|
+
const blockKey = operation.cancel.actionRef.blockIds[0]
|
|
202
|
+
if (blockKey) {
|
|
203
|
+
const redirect = await this.checkRedirect(blockKey, 'cancel', message)
|
|
204
|
+
if (redirect) {
|
|
205
|
+
response = redirect
|
|
208
206
|
} else {
|
|
209
|
-
response = await this.repo.
|
|
207
|
+
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
210
208
|
}
|
|
209
|
+
} else {
|
|
210
|
+
response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
|
|
211
|
+
}
|
|
212
|
+
} else if ('commit' in operation) {
|
|
213
|
+
const blockKey = operation.commit.tailId
|
|
214
|
+
const redirect = await this.checkRedirect(blockKey, 'commit', message)
|
|
215
|
+
if (redirect) {
|
|
216
|
+
response = redirect
|
|
217
|
+
} else {
|
|
218
|
+
response = await this.repo.commit(operation.commit, { expiration: message.expiration })
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
221
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { PeerReputationService } from './peer-reputation.js';
|
|
2
|
+
export {
|
|
3
|
+
PenaltyReason,
|
|
4
|
+
DEFAULT_PENALTY_WEIGHTS,
|
|
5
|
+
DEFAULT_THRESHOLDS,
|
|
6
|
+
type IPeerReputation,
|
|
7
|
+
type PeerReputationSummary,
|
|
8
|
+
type ReputationConfig,
|
|
9
|
+
type ReputationThresholds,
|
|
10
|
+
type PenaltyRecord,
|
|
11
|
+
type PeerRecord,
|
|
12
|
+
} from './types.js';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type IPeerReputation,
|
|
3
|
+
type PeerRecord,
|
|
4
|
+
type PenaltyRecord,
|
|
5
|
+
type PeerReputationSummary,
|
|
6
|
+
type ReputationConfig,
|
|
7
|
+
type ReputationThresholds,
|
|
8
|
+
PenaltyReason,
|
|
9
|
+
DEFAULT_PENALTY_WEIGHTS,
|
|
10
|
+
DEFAULT_THRESHOLDS,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
import { createLogger } from '../logger.js';
|
|
13
|
+
|
|
14
|
+
const log = createLogger('peer-reputation');
|
|
15
|
+
|
|
16
|
+
export class PeerReputationService implements IPeerReputation {
|
|
17
|
+
private readonly peers = new Map<string, PeerRecord>();
|
|
18
|
+
private readonly halfLifeMs: number;
|
|
19
|
+
private readonly thresholds: ReputationThresholds;
|
|
20
|
+
private readonly weights: Record<PenaltyReason, number>;
|
|
21
|
+
private readonly maxPenaltiesPerPeer: number;
|
|
22
|
+
|
|
23
|
+
constructor(config?: ReputationConfig) {
|
|
24
|
+
this.halfLifeMs = config?.halfLifeMs ?? 30 * 60_000;
|
|
25
|
+
this.thresholds = {
|
|
26
|
+
...DEFAULT_THRESHOLDS,
|
|
27
|
+
...config?.thresholds,
|
|
28
|
+
};
|
|
29
|
+
this.weights = {
|
|
30
|
+
...DEFAULT_PENALTY_WEIGHTS,
|
|
31
|
+
...config?.weights,
|
|
32
|
+
};
|
|
33
|
+
this.maxPenaltiesPerPeer = config?.maxPenaltiesPerPeer ?? 100;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
reportPeer(peerId: string, reason: PenaltyReason, context?: string): void {
|
|
37
|
+
const record = this.getOrCreateRecord(peerId);
|
|
38
|
+
const weight = this.weights[reason];
|
|
39
|
+
const penalty: PenaltyRecord = {
|
|
40
|
+
reason,
|
|
41
|
+
weight,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
context,
|
|
44
|
+
};
|
|
45
|
+
record.penalties.push(penalty);
|
|
46
|
+
record.lastPenalty = penalty.timestamp;
|
|
47
|
+
this.pruneRecord(record);
|
|
48
|
+
|
|
49
|
+
const score = this.computeScore(record);
|
|
50
|
+
log('report peerId=%s reason=%s weight=%d score=%d context=%s',
|
|
51
|
+
peerId.substring(0, 12), reason, weight, Math.round(score), context ?? '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
recordSuccess(peerId: string): void {
|
|
55
|
+
const record = this.getOrCreateRecord(peerId);
|
|
56
|
+
record.successCount++;
|
|
57
|
+
record.lastSuccess = Date.now();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getScore(peerId: string): number {
|
|
61
|
+
const record = this.peers.get(peerId);
|
|
62
|
+
if (!record) return 0;
|
|
63
|
+
return this.computeScore(record);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isBanned(peerId: string): boolean {
|
|
67
|
+
return this.getScore(peerId) >= this.thresholds.ban;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
isDeprioritized(peerId: string): boolean {
|
|
71
|
+
return this.getScore(peerId) >= this.thresholds.deprioritize;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getReputation(peerId: string): PeerReputationSummary {
|
|
75
|
+
const score = this.getScore(peerId);
|
|
76
|
+
const record = this.peers.get(peerId);
|
|
77
|
+
return {
|
|
78
|
+
peerId,
|
|
79
|
+
effectiveScore: score,
|
|
80
|
+
isBanned: score >= this.thresholds.ban,
|
|
81
|
+
isDeprioritized: score >= this.thresholds.deprioritize,
|
|
82
|
+
penaltyCount: record?.penalties.length ?? 0,
|
|
83
|
+
successCount: record?.successCount ?? 0,
|
|
84
|
+
lastPenalty: record?.lastPenalty ?? 0,
|
|
85
|
+
lastSuccess: record?.lastSuccess ?? 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getAllReputations(): Map<string, PeerReputationSummary> {
|
|
90
|
+
const result = new Map<string, PeerReputationSummary>();
|
|
91
|
+
for (const peerId of this.peers.keys()) {
|
|
92
|
+
result.set(peerId, this.getReputation(peerId));
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resetPeer(peerId: string): void {
|
|
98
|
+
this.peers.delete(peerId);
|
|
99
|
+
log('reset peerId=%s', peerId.substring(0, 12));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private getOrCreateRecord(peerId: string): PeerRecord {
|
|
103
|
+
let record = this.peers.get(peerId);
|
|
104
|
+
if (!record) {
|
|
105
|
+
record = {
|
|
106
|
+
penalties: [],
|
|
107
|
+
successCount: 0,
|
|
108
|
+
lastSuccess: 0,
|
|
109
|
+
lastPenalty: 0,
|
|
110
|
+
};
|
|
111
|
+
this.peers.set(peerId, record);
|
|
112
|
+
}
|
|
113
|
+
return record;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private computeScore(record: PeerRecord): number {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
let score = 0;
|
|
119
|
+
for (const penalty of record.penalties) {
|
|
120
|
+
score += penalty.weight * this.decayFactor(now, penalty.timestamp);
|
|
121
|
+
}
|
|
122
|
+
return score;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private decayFactor(now: number, timestamp: number): number {
|
|
126
|
+
const elapsed = now - timestamp;
|
|
127
|
+
if (elapsed <= 0) return 1;
|
|
128
|
+
return Math.pow(0.5, elapsed / this.halfLifeMs);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Remove penalties that have decayed below significance (< 1% of original weight) */
|
|
132
|
+
private pruneRecord(record: PeerRecord): void {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const cutoff = this.halfLifeMs * 7; // 2^-7 ≈ 0.8% — below significance
|
|
135
|
+
record.penalties = record.penalties.filter(p => (now - p.timestamp) < cutoff);
|
|
136
|
+
|
|
137
|
+
// Hard cap to prevent unbounded growth
|
|
138
|
+
if (record.penalties.length > this.maxPenaltiesPerPeer) {
|
|
139
|
+
record.penalties = record.penalties.slice(-this.maxPenaltiesPerPeer);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Remove peer records with no significant penalties
|
|
143
|
+
if (record.penalties.length === 0 && record.successCount === 0) {
|
|
144
|
+
// Don't remove — the caller may still be using this record
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Categories of peer misbehavior with associated severity */
|
|
2
|
+
export enum PenaltyReason {
|
|
3
|
+
/** Peer sent a signature that failed cryptographic verification */
|
|
4
|
+
InvalidSignature = 'invalid-signature',
|
|
5
|
+
/** Peer promised conflicting transactions (equivocation) */
|
|
6
|
+
Equivocation = 'equivocation',
|
|
7
|
+
/** Peer's validation logic rejected a valid transaction (repeated false rejections) */
|
|
8
|
+
FalseRejection = 'false-rejection',
|
|
9
|
+
/** Peer failed to respond within timeout during consensus */
|
|
10
|
+
ConsensusTimeout = 'consensus-timeout',
|
|
11
|
+
/** Peer sent a message with mismatched hash */
|
|
12
|
+
InvalidMessageHash = 'invalid-message-hash',
|
|
13
|
+
/** Peer sent an expired transaction */
|
|
14
|
+
ExpiredTransaction = 'expired-transaction',
|
|
15
|
+
/** Generic protocol violation */
|
|
16
|
+
ProtocolViolation = 'protocol-violation',
|
|
17
|
+
/** Connection-level failures (lighter weight) */
|
|
18
|
+
ConnectionFailure = 'connection-failure',
|
|
19
|
+
/** Majority peer approved a transaction that was later found invalid via dispute */
|
|
20
|
+
FalseApproval = 'false-approval',
|
|
21
|
+
/** Challenger lost a dispute (their rejection was wrong) */
|
|
22
|
+
DisputeLost = 'dispute-lost',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Default penalty weights by reason */
|
|
26
|
+
export const DEFAULT_PENALTY_WEIGHTS: Record<PenaltyReason, number> = {
|
|
27
|
+
[PenaltyReason.InvalidSignature]: 50,
|
|
28
|
+
[PenaltyReason.Equivocation]: 100,
|
|
29
|
+
[PenaltyReason.FalseRejection]: 10,
|
|
30
|
+
[PenaltyReason.ConsensusTimeout]: 5,
|
|
31
|
+
[PenaltyReason.InvalidMessageHash]: 50,
|
|
32
|
+
[PenaltyReason.ExpiredTransaction]: 3,
|
|
33
|
+
[PenaltyReason.ProtocolViolation]: 30,
|
|
34
|
+
[PenaltyReason.ConnectionFailure]: 2,
|
|
35
|
+
[PenaltyReason.FalseApproval]: 40,
|
|
36
|
+
[PenaltyReason.DisputeLost]: 30,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Thresholds controlling graduated reputation responses */
|
|
40
|
+
export interface ReputationThresholds {
|
|
41
|
+
/** Score above which peer is deprioritized in coordinator selection. Default: 20 */
|
|
42
|
+
deprioritize: number;
|
|
43
|
+
/** Score above which peer is excluded from cluster operations. Default: 80 */
|
|
44
|
+
ban: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_THRESHOLDS: ReputationThresholds = {
|
|
48
|
+
deprioritize: 20,
|
|
49
|
+
ban: 80,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Configuration for the reputation service */
|
|
53
|
+
export interface ReputationConfig {
|
|
54
|
+
/** Half-life for exponential decay of penalties (ms). Default: 30 minutes */
|
|
55
|
+
halfLifeMs?: number;
|
|
56
|
+
/** Thresholds for deprioritize/ban. Uses DEFAULT_THRESHOLDS if not provided */
|
|
57
|
+
thresholds?: Partial<ReputationThresholds>;
|
|
58
|
+
/** Custom penalty weights. Merged with DEFAULT_PENALTY_WEIGHTS */
|
|
59
|
+
weights?: Partial<Record<PenaltyReason, number>>;
|
|
60
|
+
/** Maximum penalty records per peer before pruning. Default: 100 */
|
|
61
|
+
maxPenaltiesPerPeer?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A single recorded penalty event */
|
|
65
|
+
export interface PenaltyRecord {
|
|
66
|
+
reason: PenaltyReason;
|
|
67
|
+
weight: number;
|
|
68
|
+
timestamp: number;
|
|
69
|
+
context?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Internal record for a tracked peer */
|
|
73
|
+
export interface PeerRecord {
|
|
74
|
+
penalties: PenaltyRecord[];
|
|
75
|
+
successCount: number;
|
|
76
|
+
lastSuccess: number;
|
|
77
|
+
lastPenalty: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Summary of a peer's reputation for diagnostics */
|
|
81
|
+
export interface PeerReputationSummary {
|
|
82
|
+
peerId: string;
|
|
83
|
+
effectiveScore: number;
|
|
84
|
+
isBanned: boolean;
|
|
85
|
+
isDeprioritized: boolean;
|
|
86
|
+
penaltyCount: number;
|
|
87
|
+
successCount: number;
|
|
88
|
+
lastPenalty: number;
|
|
89
|
+
lastSuccess: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Interface for reputation scoring consumed by other components */
|
|
93
|
+
export interface IPeerReputation {
|
|
94
|
+
/** Record a misbehavior incident */
|
|
95
|
+
reportPeer(peerId: string, reason: PenaltyReason, context?: string): void;
|
|
96
|
+
|
|
97
|
+
/** Record successful interaction */
|
|
98
|
+
recordSuccess(peerId: string): void;
|
|
99
|
+
|
|
100
|
+
/** Get effective score for a peer (0 = clean) */
|
|
101
|
+
getScore(peerId: string): number;
|
|
102
|
+
|
|
103
|
+
/** Check if peer should be excluded from operations */
|
|
104
|
+
isBanned(peerId: string): boolean;
|
|
105
|
+
|
|
106
|
+
/** Check if peer should be deprioritized */
|
|
107
|
+
isDeprioritized(peerId: string): boolean;
|
|
108
|
+
|
|
109
|
+
/** Get summary for diagnostics */
|
|
110
|
+
getReputation(peerId: string): PeerReputationSummary;
|
|
111
|
+
|
|
112
|
+
/** Get all tracked peers and their statuses */
|
|
113
|
+
getAllReputations(): Map<string, PeerReputationSummary>;
|
|
114
|
+
|
|
115
|
+
/** Reset a peer's reputation (admin/testing) */
|
|
116
|
+
resetPeer(peerId: string): void;
|
|
117
|
+
}
|
|
@@ -118,6 +118,17 @@ export class ArachnodeFretAdapter {
|
|
|
118
118
|
.sort((a, b) => a.ringDepth - b.ringDepth);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Update the status field of this node's ArachnodeInfo.
|
|
123
|
+
* No-op if no ArachnodeInfo has been set yet.
|
|
124
|
+
*/
|
|
125
|
+
setStatus(status: ArachnodeInfo['status']): void {
|
|
126
|
+
const current = this.getMyArachnodeInfo();
|
|
127
|
+
if (current) {
|
|
128
|
+
this.setArachnodeInfo({ ...current, status });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
/**
|
|
122
133
|
* Access the underlying FRET service.
|
|
123
134
|
*/
|
|
@@ -4,6 +4,9 @@ import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from
|
|
|
4
4
|
import type { IRawStorage } from "./i-raw-storage.js";
|
|
5
5
|
import { mergeRanges } from "./helpers.js";
|
|
6
6
|
import type { IBlockStorage } from "./i-block-storage.js";
|
|
7
|
+
import { createLogger } from "../logger.js";
|
|
8
|
+
|
|
9
|
+
const log = createLogger('block-storage');
|
|
7
10
|
|
|
8
11
|
export class BlockStorage implements IBlockStorage {
|
|
9
12
|
constructor(
|
|
@@ -45,6 +48,7 @@ export class BlockStorage implements IBlockStorage {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
async savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void> {
|
|
51
|
+
log('pend blockId=%s actionId=%s', this.blockId, actionId);
|
|
48
52
|
let meta = await this.storage.getMetadata(this.blockId);
|
|
49
53
|
if (!meta) {
|
|
50
54
|
meta = { latest: undefined, ranges: [[0]] };
|
|
@@ -54,6 +58,7 @@ export class BlockStorage implements IBlockStorage {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
async deletePendingTransaction(actionId: ActionId): Promise<void> {
|
|
61
|
+
log('cancel blockId=%s actionId=%s', this.blockId, actionId);
|
|
57
62
|
await this.storage.deletePendingTransaction(this.blockId, actionId);
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -70,6 +75,7 @@ export class BlockStorage implements IBlockStorage {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
async promotePendingTransaction(actionId: ActionId): Promise<void> {
|
|
78
|
+
log('commit blockId=%s actionId=%s', this.blockId, actionId);
|
|
73
79
|
await this.storage.promotePendingTransaction(this.blockId, actionId);
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from "@optimystic/db-core";
|
|
12
12
|
import { asyncIteratorToArray } from "../it-utility.js";
|
|
13
13
|
import type { IBlockStorage } from "./i-block-storage.js";
|
|
14
|
+
import { createLogger } from "../logger.js";
|
|
15
|
+
|
|
16
|
+
const log = createLogger('storage-repo');
|
|
14
17
|
|
|
15
18
|
export type StorageRepoOptions = {
|
|
16
19
|
/** Optional hook to validate transactions in PendRequests */
|
|
@@ -29,6 +32,7 @@ export class StorageRepo implements IRepo {
|
|
|
29
32
|
|
|
30
33
|
async get({ blockIds, context }: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
|
|
31
34
|
const distinctBlockIds = Array.from(new Set(blockIds));
|
|
35
|
+
log('get blockIds=%d', distinctBlockIds.length);
|
|
32
36
|
const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
|
|
33
37
|
const blockStorage = this.createBlockStorage(blockId);
|
|
34
38
|
|
|
@@ -92,6 +96,7 @@ export class StorageRepo implements IRepo {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
const blockIds = blockIdsForTransforms(request.transforms);
|
|
99
|
+
log('pend actionId=%s blockIds=%d rev=%s', request.actionId, blockIds.length, request.rev);
|
|
95
100
|
const pendings: ActionPending[] = [];
|
|
96
101
|
const missing: ActionTransforms[] = [];
|
|
97
102
|
|
|
@@ -131,6 +136,7 @@ export class StorageRepo implements IRepo {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
if (missing.length) {
|
|
139
|
+
log('pend:stale actionId=%s missing=%d', request.actionId, missing.length);
|
|
134
140
|
return {
|
|
135
141
|
success: false,
|
|
136
142
|
missing
|
|
@@ -175,6 +181,7 @@ export class StorageRepo implements IRepo {
|
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
async cancel(actionRef: ActionBlocks, _options?: MessageOptions): Promise<void> {
|
|
184
|
+
log('cancel actionId=%s blockIds=%d', actionRef.actionId, actionRef.blockIds.length);
|
|
178
185
|
await Promise.all(actionRef.blockIds.map(blockId => {
|
|
179
186
|
const blockStorage = this.createBlockStorage(blockId);
|
|
180
187
|
return blockStorage.deletePendingTransaction(actionRef.actionId);
|
|
@@ -182,6 +189,7 @@ export class StorageRepo implements IRepo {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
|
|
192
|
+
log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
|
|
185
193
|
const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
|
|
186
194
|
const releases: (() => void)[] = [];
|
|
187
195
|
|
|
@@ -222,6 +230,7 @@ export class StorageRepo implements IRepo {
|
|
|
222
230
|
}
|
|
223
231
|
|
|
224
232
|
if (missedCommits.length) {
|
|
233
|
+
log('commit:stale actionId=%s missed=%d', request.actionId, missedCommits.length);
|
|
225
234
|
return { // Return directly, locks will be released in finally
|
|
226
235
|
success: false,
|
|
227
236
|
missing: perBlockActionTransformsToPerAction(missedCommits)
|