@optimystic/db-p2p 0.11.2 → 0.12.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 +0 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +0 -2
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +1 -2
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +5 -5
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/partition-detector.d.ts.map +1 -1
- package/dist/src/cluster/partition-detector.js +0 -1
- package/dist/src/cluster/partition-detector.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/service.d.ts +0 -6
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +0 -13
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +1 -1
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +14 -1
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/network-manager-service.js +1 -1
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +3 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +48 -17
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +0 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +0 -2
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/routing/simple-cluster-coordinator.d.ts +2 -2
- package/dist/src/routing/simple-cluster-coordinator.d.ts.map +1 -1
- package/dist/src/routing/simple-cluster-coordinator.js +2 -2
- package/dist/src/routing/simple-cluster-coordinator.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +4 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +29 -0
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +17 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +9 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.d.ts +18 -0
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +47 -2
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/ring-selector.d.ts.map +1 -1
- package/dist/src/storage/ring-selector.js +23 -5
- package/dist/src/storage/ring-selector.js.map +1 -1
- package/dist/src/storage/storage-monitor.d.ts.map +1 -1
- package/dist/src/storage/storage-monitor.js +10 -14
- package/dist/src/storage/storage-monitor.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +10 -2
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +39 -10
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/service.d.ts +0 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +0 -2
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/index.d.ts +2 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +2 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/mesh-harness.d.ts +60 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/mesh-harness.js +220 -0
- package/dist/src/testing/mesh-harness.js.map +1 -0
- package/package.json +8 -3
- package/src/cluster/block-transfer-service.ts +1 -1
- package/src/cluster/cluster-repo.ts +5 -3
- package/src/cluster/partition-detector.ts +0 -2
- package/src/cluster/rebalance-monitor.ts +1 -1
- package/src/cluster/service.ts +0 -13
- package/src/dispute/dispute-service.ts +1 -2
- package/src/dispute/service.ts +1 -1
- package/src/libp2p-key-network.ts +1 -1
- package/src/libp2p-node-base.ts +13 -3
- package/src/network/network-manager-service.ts +1 -1
- package/src/repo/cluster-coordinator.ts +4 -4
- package/src/repo/coordinator-repo.ts +51 -19
- package/src/repo/service.ts +0 -2
- package/src/routing/simple-cluster-coordinator.ts +2 -2
- package/src/storage/block-storage.ts +32 -0
- package/src/storage/i-block-storage.ts +15 -0
- package/src/storage/i-raw-storage.ts +10 -0
- package/src/storage/memory-storage.ts +48 -2
- package/src/storage/ring-selector.ts +26 -5
- package/src/storage/storage-monitor.ts +10 -17
- package/src/storage/storage-repo.ts +40 -11
- package/src/sync/service.ts +2 -3
- package/src/testing/index.ts +1 -0
- package/src/testing/mesh-harness.ts +294 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
|
+
import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId, ActionRev, ClusterConsensusConfig, ITransactor, PeerId as DbPeerId } from '@optimystic/db-core';
|
|
3
|
+
import type { FindCoordinatorOptions } from '@optimystic/db-core';
|
|
4
|
+
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
5
|
+
import { NetworkTransactor } from '@optimystic/db-core';
|
|
6
|
+
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
7
|
+
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
8
|
+
import { ClusterMember, clusterMember } from '../cluster/cluster-repo.js';
|
|
9
|
+
import { StorageRepo } from '../storage/storage-repo.js';
|
|
10
|
+
import { MemoryRawStorage } from '../storage/memory-storage.js';
|
|
11
|
+
import { BlockStorage } from '../storage/block-storage.js';
|
|
12
|
+
import type { IRawStorage } from '../storage/i-raw-storage.js';
|
|
13
|
+
import { coordinatorRepo, type ClusterLatestCallback } from '../repo/coordinator-repo.js';
|
|
14
|
+
import type { CoordinatorRepo } from '../repo/coordinator-repo.js';
|
|
15
|
+
import { sortPeersByDistance, type KnownPeer } from '../routing/responsibility.js';
|
|
16
|
+
import { toString as u8ToString } from 'uint8arrays';
|
|
17
|
+
|
|
18
|
+
export interface MeshNode {
|
|
19
|
+
peerId: PeerId;
|
|
20
|
+
privateKey: PrivateKey;
|
|
21
|
+
storageRepo: StorageRepo;
|
|
22
|
+
clusterMember: ClusterMember;
|
|
23
|
+
coordinatorRepo: CoordinatorRepo;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MeshOptions {
|
|
27
|
+
responsibilityK: number;
|
|
28
|
+
clusterSize?: number;
|
|
29
|
+
superMajorityThreshold?: number;
|
|
30
|
+
allowClusterDownsize?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Optional per-node raw-storage factory. Invoked once per node (indexed from 0)
|
|
33
|
+
* to supply the IRawStorage that backs StorageRepo. If omitted, each node gets
|
|
34
|
+
* a fresh `MemoryRawStorage`. Used by fault-injection tests to wrap the store
|
|
35
|
+
* with a crashing proxy, or by restart tests to rebuild over preserved state.
|
|
36
|
+
*/
|
|
37
|
+
rawStorageFactory?: (index: number) => IRawStorage;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MeshFailureConfig {
|
|
41
|
+
/** Peers that should fail on cluster update (simulate unreachable) */
|
|
42
|
+
failingPeers?: Set<string>;
|
|
43
|
+
/** Make findCluster return empty (simulate DHT failure) */
|
|
44
|
+
findClusterFails?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class MockPeerNetwork implements IPeerNetwork {
|
|
48
|
+
async connect(_peerId: PeerId, _protocol: string): Promise<any> {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mock IKeyNetwork that returns peers based on XOR distance.
|
|
55
|
+
* With responsibilityK >= nodeCount, all nodes are returned.
|
|
56
|
+
* Otherwise, K-nearest by XOR distance are returned.
|
|
57
|
+
*/
|
|
58
|
+
class MockMeshKeyNetwork implements IKeyNetwork {
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly nodes: MeshNode[],
|
|
61
|
+
private readonly responsibilityK: number,
|
|
62
|
+
private readonly failures: MeshFailureConfig = {}
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
async findCoordinator(key: Uint8Array, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
|
|
66
|
+
const excluded = new Set((options?.excludedPeers ?? []).map(p => p.toString()));
|
|
67
|
+
const sorted = this.sortedByDistance(key);
|
|
68
|
+
const pick = sorted.find(n => !excluded.has(n.peerId.toString()));
|
|
69
|
+
if (!pick) {
|
|
70
|
+
throw new Error('No coordinator available for key (all candidates excluded)');
|
|
71
|
+
}
|
|
72
|
+
return pick.peerId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async findCluster(key: Uint8Array): Promise<ClusterPeers> {
|
|
76
|
+
if (this.failures.findClusterFails) {
|
|
77
|
+
return {} as ClusterPeers;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sorted = this.sortedByDistance(key);
|
|
81
|
+
const k = Math.min(this.responsibilityK, sorted.length);
|
|
82
|
+
const selected = sorted.slice(0, k);
|
|
83
|
+
|
|
84
|
+
const peers: ClusterPeers = {};
|
|
85
|
+
for (const node of selected) {
|
|
86
|
+
peers[node.peerId.toString()] = {
|
|
87
|
+
multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
|
|
88
|
+
publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return peers;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private sortedByDistance(key: Uint8Array): MeshNode[] {
|
|
95
|
+
const knownPeers: KnownPeer[] = this.nodes.map(n => ({
|
|
96
|
+
id: n.peerId,
|
|
97
|
+
addrs: ['/ip4/127.0.0.1/tcp/8000']
|
|
98
|
+
}));
|
|
99
|
+
const sorted = sortPeersByDistance(knownPeers, key);
|
|
100
|
+
return sorted.map(kp => this.nodes.find(n => n.peerId.equals(kp.id))!);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface Mesh {
|
|
105
|
+
nodes: MeshNode[];
|
|
106
|
+
failures: MeshFailureConfig;
|
|
107
|
+
keyNetwork: IKeyNetwork;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates N interconnected mesh nodes with real components and mock transport.
|
|
112
|
+
* ClusterClient calls route directly to target ClusterMember instances.
|
|
113
|
+
*/
|
|
114
|
+
export async function createMesh(nodeCount: number, options: MeshOptions): Promise<Mesh> {
|
|
115
|
+
const failures: MeshFailureConfig = {};
|
|
116
|
+
|
|
117
|
+
// Generate key pairs for all nodes
|
|
118
|
+
const keyPairs = await Promise.all(
|
|
119
|
+
Array.from({ length: nodeCount }, async () => {
|
|
120
|
+
const privateKey = await generateKeyPair('Ed25519');
|
|
121
|
+
return { peerId: peerIdFromPrivateKey(privateKey), privateKey };
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Build nodes array (partially — coordinatorRepo added after keyNetwork is ready)
|
|
126
|
+
const nodes: MeshNode[] = [];
|
|
127
|
+
const peerNetwork = new MockPeerNetwork();
|
|
128
|
+
// Map peerId → rawStorage for data sync simulation in clusterLatestCallback
|
|
129
|
+
const rawStorages = new Map<string, IRawStorage>();
|
|
130
|
+
|
|
131
|
+
// Phase 1: create storage + cluster members
|
|
132
|
+
let nodeIndex = 0;
|
|
133
|
+
for (const { peerId, privateKey } of keyPairs) {
|
|
134
|
+
const rawStorage = options.rawStorageFactory
|
|
135
|
+
? options.rawStorageFactory(nodeIndex)
|
|
136
|
+
: new MemoryRawStorage();
|
|
137
|
+
rawStorages.set(peerId.toString(), rawStorage);
|
|
138
|
+
nodeIndex++;
|
|
139
|
+
const storageRepo = new StorageRepo(
|
|
140
|
+
(blockId: BlockId) => new BlockStorage(blockId, rawStorage)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const consensusConfig: ClusterConsensusConfig = {
|
|
144
|
+
superMajorityThreshold: options.superMajorityThreshold ?? 0.75,
|
|
145
|
+
simpleMajorityThreshold: 0.51,
|
|
146
|
+
minAbsoluteClusterSize: 2,
|
|
147
|
+
allowClusterDownsize: options.allowClusterDownsize ?? true,
|
|
148
|
+
clusterSizeTolerance: 0.5,
|
|
149
|
+
partitionDetectionWindow: 60000
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const member = clusterMember({
|
|
153
|
+
storageRepo,
|
|
154
|
+
peerNetwork,
|
|
155
|
+
peerId,
|
|
156
|
+
privateKey,
|
|
157
|
+
consensusConfig
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
nodes.push({
|
|
161
|
+
peerId,
|
|
162
|
+
privateKey,
|
|
163
|
+
storageRepo,
|
|
164
|
+
clusterMember: member,
|
|
165
|
+
coordinatorRepo: undefined as any // filled in phase 2
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Phase 2: create key network and coordinator repos (needs all nodes for routing)
|
|
170
|
+
const keyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
|
|
171
|
+
|
|
172
|
+
const createClusterClient = (targetPeerId: PeerId): ICluster => {
|
|
173
|
+
const target = nodes.find(n => n.peerId.equals(targetPeerId));
|
|
174
|
+
if (!target) {
|
|
175
|
+
throw new Error(`Unknown peer: ${targetPeerId.toString()}`);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
async update(record: ClusterRecord): Promise<ClusterRecord> {
|
|
179
|
+
if (failures.failingPeers?.has(targetPeerId.toString())) {
|
|
180
|
+
throw new Error(`Peer ${targetPeerId.toString()} is unreachable`);
|
|
181
|
+
}
|
|
182
|
+
return target.clusterMember.update(record);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
for (const node of nodes) {
|
|
188
|
+
const localRawStorage = rawStorages.get(node.peerId.toString())!;
|
|
189
|
+
|
|
190
|
+
// Per-node callback: queries remote peer and replicates committed data locally
|
|
191
|
+
// (simulates what SyncClient does in production)
|
|
192
|
+
const clusterLatestCallback: ClusterLatestCallback = async (peerId: PeerId, blockId: BlockId, context?): Promise<ActionRev | undefined> => {
|
|
193
|
+
const target = nodes.find(n => n.peerId.equals(peerId));
|
|
194
|
+
if (!target) return undefined;
|
|
195
|
+
const result = await target.storageRepo.get(
|
|
196
|
+
{ blockIds: [blockId], context },
|
|
197
|
+
{ skipClusterFetch: true } as any
|
|
198
|
+
);
|
|
199
|
+
const entry = result[blockId];
|
|
200
|
+
const latest = entry?.state?.latest;
|
|
201
|
+
|
|
202
|
+
// Simulate data sync: replicate committed block data to local storage
|
|
203
|
+
if (latest && entry?.block) {
|
|
204
|
+
const localBlockStorage = new BlockStorage(blockId as BlockId, localRawStorage);
|
|
205
|
+
const localLatest = await localBlockStorage.getLatest();
|
|
206
|
+
if (!localLatest || localLatest.rev < latest.rev) {
|
|
207
|
+
// Ensure metadata exists
|
|
208
|
+
const meta = await localRawStorage.getMetadata(blockId as BlockId);
|
|
209
|
+
if (!meta) {
|
|
210
|
+
await localRawStorage.saveMetadata(blockId as BlockId, { latest: undefined, ranges: [[0]] });
|
|
211
|
+
}
|
|
212
|
+
await localBlockStorage.saveMaterializedBlock(latest.actionId, entry.block);
|
|
213
|
+
await localBlockStorage.saveRevision(latest.rev, latest.actionId);
|
|
214
|
+
await localBlockStorage.setLatest(latest);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return latest;
|
|
219
|
+
};
|
|
220
|
+
// Wrap key network to include self in findCluster (matches real Libp2pKeyPeerNetwork behavior)
|
|
221
|
+
const nodeKeyNetwork: IKeyNetwork = {
|
|
222
|
+
findCoordinator: (key, opts) => keyNetwork.findCoordinator(key, opts),
|
|
223
|
+
async findCluster(key) {
|
|
224
|
+
const peers = await keyNetwork.findCluster(key);
|
|
225
|
+
const selfStr = node.peerId.toString();
|
|
226
|
+
if (!(selfStr in peers)) {
|
|
227
|
+
peers[selfStr] = {
|
|
228
|
+
multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
|
|
229
|
+
publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return peers;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const factory = coordinatorRepo(
|
|
236
|
+
nodeKeyNetwork,
|
|
237
|
+
(peerId: PeerId) => createClusterClient(peerId) as any,
|
|
238
|
+
{
|
|
239
|
+
clusterSize: options.clusterSize ?? nodeCount,
|
|
240
|
+
superMajorityThreshold: options.superMajorityThreshold ?? 0.75,
|
|
241
|
+
allowClusterDownsize: options.allowClusterDownsize ?? true
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
node.coordinatorRepo = factory({
|
|
245
|
+
storageRepo: node.storageRepo,
|
|
246
|
+
localCluster: node.clusterMember,
|
|
247
|
+
localPeerId: node.peerId,
|
|
248
|
+
clusterLatestCallback
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { nodes, failures, keyNetwork };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface BuildTransactorOptions {
|
|
256
|
+
timeoutMs?: number;
|
|
257
|
+
abortOrCancelTimeoutMs?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Builds a NetworkTransactor over a mesh. All nodes share the same mock
|
|
262
|
+
* infrastructure so a single transactor routes to every peer via `getRepo`.
|
|
263
|
+
* Suitable for solo-mesh tests; for multi-node tests prefer
|
|
264
|
+
* `buildNetworkTransactors` to label "which node is driving".
|
|
265
|
+
*/
|
|
266
|
+
export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor => {
|
|
267
|
+
const repoByPeer = new Map<string, IRepo>();
|
|
268
|
+
for (const node of mesh.nodes) {
|
|
269
|
+
repoByPeer.set(node.peerId.toString(), node.coordinatorRepo as unknown as IRepo);
|
|
270
|
+
}
|
|
271
|
+
return new NetworkTransactor({
|
|
272
|
+
timeoutMs: options.timeoutMs ?? 5_000,
|
|
273
|
+
abortOrCancelTimeoutMs: options.abortOrCancelTimeoutMs ?? 5_000,
|
|
274
|
+
keyNetwork: mesh.keyNetwork,
|
|
275
|
+
getRepo: (peerId: DbPeerId) => {
|
|
276
|
+
const repo = repoByPeer.get(peerId.toString());
|
|
277
|
+
if (!repo) throw new Error(`Unknown peer ${peerId.toString()}`);
|
|
278
|
+
return repo;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Builds one NetworkTransactor per mesh node, keyed by peer-id string. Each
|
|
285
|
+
* transactor shares the mesh's key network and peer→repo map — the separate
|
|
286
|
+
* instances exist so tests can semantically say "driven by node A".
|
|
287
|
+
*/
|
|
288
|
+
export const buildNetworkTransactors = (mesh: Mesh, options: BuildTransactorOptions = {}): Map<string, ITransactor> => {
|
|
289
|
+
const transactors = new Map<string, ITransactor>();
|
|
290
|
+
for (const node of mesh.nodes) {
|
|
291
|
+
transactors.set(node.peerId.toString(), buildNetworkTransactor(mesh, options));
|
|
292
|
+
}
|
|
293
|
+
return transactors;
|
|
294
|
+
};
|