@optimystic/db-p2p 0.28.0 → 1.0.0-beta.1
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 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +4 -2
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +101 -18
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +153 -30
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +75 -16
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +195 -24
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/quorum-restore.d.ts +4 -2
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +4 -2
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/service.d.ts +12 -9
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +6 -6
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/dispute/service.d.ts +1 -4
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +8 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +8 -2
- 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 +22 -19
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +28 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +143 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +1 -4
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +2 -1
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +16 -1
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +66 -7
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +188 -14
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +714 -110
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +9 -6
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +4 -5
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +6 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +62 -2
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/service.d.ts +1 -2
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +2 -1
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +7 -1
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +2 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +19 -0
- package/src/cluster/block-transfer-service.ts +4 -8
- package/src/cluster/cluster-policy.ts +196 -36
- package/src/cluster/cluster-repo.ts +2662 -2488
- package/src/cluster/quorum-restore.ts +4 -2
- package/src/cluster/service.ts +14 -9
- package/src/dispute/service.ts +3 -3
- package/src/libp2p-key-network.ts +8 -2
- package/src/libp2p-node-base.ts +22 -19
- package/src/logger.ts +196 -2
- package/src/network/network-manager-service.ts +414 -414
- package/src/protocol-client.ts +196 -196
- package/src/repo/cluster-coordinator.ts +1113 -1039
- package/src/repo/coordinator-repo.ts +2675 -1937
- package/src/repo/service.ts +12 -9
- package/src/storage/storage-repo.ts +65 -2
- package/src/sync/service.ts +3 -5
- package/src/testing/mesh-harness.ts +8 -1
package/src/repo/service.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { pipe } from 'it-pipe'
|
|
2
2
|
import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
|
|
3
|
-
import type { Startable,
|
|
3
|
+
import type { Startable, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
|
|
4
4
|
import type { IRepo, RepoMessage } from '@optimystic/db-core'
|
|
5
5
|
import { blockIdsForTransforms } from '@optimystic/db-core'
|
|
6
6
|
import { peersEqual } from '../peer-utils.js'
|
|
7
7
|
import { encodePeers, type RedirectPayload } from './redirect.js'
|
|
8
8
|
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js'
|
|
9
9
|
import type { Uint8ArrayList } from 'uint8arraylist'
|
|
10
|
-
import { createLogger } from '../logger.js'
|
|
10
|
+
import { createLogger, type Logger } from '../logger.js'
|
|
11
11
|
import { publishableAddrsForPeer, type AddressLog, type DirectionalConnection } from '../peer-address-book.js'
|
|
12
12
|
import { createInboundStreamAuthorization, type InboundStreamAuthorization, type InboundStreamAuthorizationInit } from '../inbound-authorization.js'
|
|
13
13
|
import { registerProtocolHandler } from '../network/register-protocol-handler.js'
|
|
14
14
|
|
|
15
|
-
const debugLog = createLogger('repo-service')
|
|
16
|
-
|
|
17
15
|
// Define Components interface
|
|
18
16
|
interface BaseComponents {
|
|
19
|
-
logger: { forComponent: (name: string) => Logger },
|
|
20
17
|
registrar: {
|
|
21
18
|
handle: (protocol: string, handler: StreamHandler, options: any) => Promise<void>
|
|
22
19
|
unhandle: (protocol: string) => Promise<void>
|
|
@@ -52,6 +49,12 @@ export type RepoServiceInit = InboundStreamAuthorizationInit & {
|
|
|
52
49
|
protocolPrefix?: string,
|
|
53
50
|
maxInboundStreams?: number,
|
|
54
51
|
maxOutboundStreams?: number,
|
|
52
|
+
/**
|
|
53
|
+
* Sub-namespace this service logs under, i.e. the `<x>` in `optimystic:db-p2p:<x>`.
|
|
54
|
+
* NOT a full namespace: an embedder cannot use this to move the service's lines outside the
|
|
55
|
+
* `optimystic:db-p2p:*` tree `docs/debugging.md` tells operators to filter on.
|
|
56
|
+
* Default: `repo-service`.
|
|
57
|
+
*/
|
|
55
58
|
logPrefix?: string,
|
|
56
59
|
kBucketSize?: number,
|
|
57
60
|
/**
|
|
@@ -94,8 +97,8 @@ export class RepoService implements Startable {
|
|
|
94
97
|
private readonly authorization: InboundStreamAuthorization | undefined
|
|
95
98
|
/**
|
|
96
99
|
* Sink for this service's `peer-address-book:*` lines — same reasoning as `ClusterService`'s:
|
|
97
|
-
* `this.log.error` would
|
|
98
|
-
*
|
|
100
|
+
* `this.log.error` would scatter one tag family across whichever service happened to be the
|
|
101
|
+
* ingress point, instead of keeping it filterable as the single `peer-address-book` namespace.
|
|
99
102
|
*/
|
|
100
103
|
private readonly addressLog: AddressLog
|
|
101
104
|
|
|
@@ -105,7 +108,7 @@ export class RepoService implements Startable {
|
|
|
105
108
|
this.protocol = computed
|
|
106
109
|
this.maxInboundStreams = init.maxInboundStreams ?? 32
|
|
107
110
|
this.maxOutboundStreams = init.maxOutboundStreams ?? 64
|
|
108
|
-
this.log =
|
|
111
|
+
this.log = createLogger(init.logPrefix ?? 'repo-service')
|
|
109
112
|
this.addressLog = createLogger('peer-address-book', components.peerId?.toString())
|
|
110
113
|
this.repo = components.repo
|
|
111
114
|
this.running = false
|
|
@@ -246,7 +249,7 @@ export class RepoService implements Startable {
|
|
|
246
249
|
|
|
247
250
|
if (!smallMesh && !isMember) {
|
|
248
251
|
const peers = cluster.filter((p: PeerId) => !peersEqual(p, selfId))
|
|
249
|
-
|
|
252
|
+
this.log('redirect op=%s blockKey=%s cluster=%d', opName, blockKey, cluster.length)
|
|
250
253
|
return encodePeers(await Promise.all(peers.map(async (pid: PeerId) => ({
|
|
251
254
|
id: pid.toString(),
|
|
252
255
|
addrs: await this.getPeerAddrs(pid)
|
|
@@ -281,6 +281,15 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
281
281
|
// Sort a COPY: when `latest` is undefined, `missing` aliases the caller's
|
|
282
282
|
// `context.committed` array, and an in-place `.sort()` would reorder the shared
|
|
283
283
|
// request context under the caller's feet.
|
|
284
|
+
//
|
|
285
|
+
// NOTE: this loop SKIPS an entry whose pending it does not hold and promotes the
|
|
286
|
+
// next one anyway — so a member missing an intermediate revision that touched THIS
|
|
287
|
+
// block forks it exactly as an un-guarded commit would. `internalCommit`'s declared-
|
|
288
|
+
// base guard cannot help here: `context.committed` is a collection-level list of
|
|
289
|
+
// (actionId, rev) with no per-block base, and "no pending for that action" is the
|
|
290
|
+
// normal case for the many actions that never touched this block. Closing it needs
|
|
291
|
+
// the authored base stored WITH the pended transform — tracked by
|
|
292
|
+
// `backlog/bug-a-pended-transform-does-not-carry-its-base`.
|
|
284
293
|
try {
|
|
285
294
|
for (const { actionId, rev } of [...missing].sort((a, b) => a.rev - b.rev)) {
|
|
286
295
|
const pending = await blockStorage.getPendingTransaction(actionId);
|
|
@@ -905,7 +914,11 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
905
914
|
}
|
|
906
915
|
try {
|
|
907
916
|
// internalCommit will throw if it encounters an issue
|
|
908
|
-
|
|
917
|
+
// The writer's per-block base declaration (see BlockContentDigest.baseRev) rides on the
|
|
918
|
+
// commit op and is what lets internalCommit tell a legitimate collection-level rev gap
|
|
919
|
+
// apart from a genuinely missed update to THIS block. Untrusted wire data — the guard
|
|
920
|
+
// validates it, this call site only forwards it.
|
|
921
|
+
const collectionId = await this.internalCommit(blockId, request.actionId, request.rev, storage, latch, proof, request.blockDigests?.[blockId]?.baseRev);
|
|
909
922
|
if (collectionId !== undefined) {
|
|
910
923
|
const list = collectionBlocks.get(collectionId) ?? [];
|
|
911
924
|
list.push(blockId);
|
|
@@ -1174,7 +1187,13 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
1174
1187
|
return await this.createBlockStorage(blockId).getBlockProof(rev);
|
|
1175
1188
|
}
|
|
1176
1189
|
|
|
1177
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* @param declaredBaseRev The committed revision of the base the WRITER applied this block's
|
|
1192
|
+
* transform to, as declared in the commit op's `blockDigests[blockId].baseRev`. Untrusted wire
|
|
1193
|
+
* data, so it is typed `unknown` and validated below. Absent from the read-driven promotion in
|
|
1194
|
+
* {@link get}, which has no commit request and therefore nothing to compare against.
|
|
1195
|
+
*/
|
|
1196
|
+
private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage, latch: BlockWriteLatch, proof?: BlockCommitProof, declaredBaseRev?: unknown): Promise<CollectionId | undefined> {
|
|
1178
1197
|
// Note: This method is called under the block write latch — by commit() (within its locked
|
|
1179
1198
|
// critical section) and by the read-driven promotion in get() (which takes the same latch);
|
|
1180
1199
|
// `latch` is the proof of that. So, operations like getPendingTransaction, getLatest,
|
|
@@ -1194,6 +1213,50 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
|
|
|
1194
1213
|
|
|
1195
1214
|
// Get prior materialized block if it exists
|
|
1196
1215
|
const latest = await storage.getLatest();
|
|
1216
|
+
|
|
1217
|
+
// FORK GUARD: apply an update-only transform ONLY to the base the writer actually read.
|
|
1218
|
+
// Revisions are allocated per COLLECTION, not per block, so `rev - 1` is meaningless here — a
|
|
1219
|
+
// member legitimately holds block X at rev 1 and receives a commit of X at rev 7 when revs 2-6
|
|
1220
|
+
// touched other blocks. The only sound discriminator is the writer's own per-block declaration:
|
|
1221
|
+
// it read the block at `declaredBaseRev`, so a member holding anything else would be applying
|
|
1222
|
+
// the transform to different bytes and silently forking the block's content at this revision.
|
|
1223
|
+
//
|
|
1224
|
+
// Each clause is deliberate:
|
|
1225
|
+
// - `typeof declaredBaseRev === 'number'` — `blockDigests` is untrusted wire data with no
|
|
1226
|
+
// ingress schema (same rule as ClusterMember.validateCommitOperations). Missing, malformed,
|
|
1227
|
+
// or absent-by-design declarations ABSTAIN, preserving today's behavior for pre-upgrade
|
|
1228
|
+
// writers, undeclarable blocks, and the read-driven promotion in get().
|
|
1229
|
+
// - `!transform.insert` — an insert-carrying transform is base-independent, so there is nothing
|
|
1230
|
+
// to fork. Keyed on the member's OWN pended transform (as previewCommitDigest does), never on
|
|
1231
|
+
// the declaration, so a hostile writer cannot flip the arm by attaching a bogus baseRev.
|
|
1232
|
+
// - `latest?.rev !== declaredBaseRev` covers all three unsafe states: BEHIND the declared base
|
|
1233
|
+
// (missed updates — the fork case), AHEAD of it (this member holds a revision the writer
|
|
1234
|
+
// never saw — divergent history), and no local revision at all against a numeric declaration.
|
|
1235
|
+
//
|
|
1236
|
+
// Refusing is cheap and self-healing: refuseMissingBase throws MissingBaseRevisionError, which
|
|
1237
|
+
// commit() classifies as divergence and ClusterMember.applyConsensusOperation maps to "behind",
|
|
1238
|
+
// running reconcileDivergentCommit to pull the committed revision from a cohort peer. The
|
|
1239
|
+
// writer's retry then lands on a healed base. A hostile writer declaring a junk numeric baseRev
|
|
1240
|
+
// can force refusals and reconcile churn, but never a fork.
|
|
1241
|
+
//
|
|
1242
|
+
// NOTE: the AHEAD case is reported as "behind" divergence like every other missing-base
|
|
1243
|
+
// refusal, so a cohort where nobody holds `rev` reconciles, fails `no-rev-quorum`, and logs
|
|
1244
|
+
// that rather than a clean stale failure. Correct outcome — the writer read a base the cohort
|
|
1245
|
+
// has moved past, and its retry re-reads — but the log reads as lag when it is the opposite.
|
|
1246
|
+
// If those lines ever have to be triaged in volume, give the ahead arm its own reason string.
|
|
1247
|
+
//
|
|
1248
|
+
// NOTE: this guard only reaches what the writer declared, so two arms of the same fork still
|
|
1249
|
+
// stand — both tracked by `backlog/bug-a-pended-transform-does-not-carry-its-base`:
|
|
1250
|
+
// (1) a commit whose block declares NO digest — pre-upgrade writer, undeclarable block
|
|
1251
|
+
// (read-far-then-update eviction, see db-core transform/digest.ts), or a delete-only transform —
|
|
1252
|
+
// gap-applies exactly as before; (2) the read-driven promotion in `get`, which reaches this
|
|
1253
|
+
// method with no commit request at all (`declaredBaseRev` undefined) and so cannot check. If
|
|
1254
|
+
// forked-content reports persist, those are the residuals to look at.
|
|
1255
|
+
if (typeof declaredBaseRev === 'number' && !transform.insert && latest?.rev !== declaredBaseRev) {
|
|
1256
|
+
return await this.refuseMissingBase(blockId, actionId, rev, storage, latch,
|
|
1257
|
+
`local latest ${latest?.rev ?? 'none'} is not the declared base ${declaredBaseRev} of rev ${rev}`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1197
1260
|
const priorBlock = await this.readCommitBase(blockId, actionId, rev, storage, latest, latch);
|
|
1198
1261
|
|
|
1199
1262
|
// Apply transform and save materialized block
|
package/src/sync/service.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Connection, Startable } from '@libp2p/interface';
|
|
2
2
|
import { buildSyncProtocol, type SyncRequest, type SyncResponse } from './protocol.js';
|
|
3
3
|
import { pipe } from 'it-pipe';
|
|
4
4
|
import { toString as u8ToString } from 'uint8arrays/to-string';
|
|
@@ -9,13 +9,13 @@ import { createInboundStreamAuthorization, type InboundStreamAuthorization, type
|
|
|
9
9
|
import { serveBlockArchive, type ArchiveServingRepo } from '../storage/block-archive.js';
|
|
10
10
|
import { registerProtocolHandler } from '../network/register-protocol-handler.js';
|
|
11
11
|
import type { BlockArchive } from '../storage/struct.js';
|
|
12
|
+
import { createLogger, type Logger } from '../logger.js';
|
|
12
13
|
|
|
13
14
|
export interface SyncServiceInit extends InboundStreamAuthorizationInit {
|
|
14
15
|
protocolPrefix?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export interface SyncServiceComponents {
|
|
18
|
-
logger: ComponentLogger;
|
|
19
19
|
registrar: { handle: (...args: any[]) => Promise<void>, unhandle: (...args: any[]) => Promise<void> };
|
|
20
20
|
/**
|
|
21
21
|
* The local store this node answers repair fetches out of. Declared as `ArchiveServingRepo`
|
|
@@ -28,8 +28,6 @@ export interface SyncServiceComponents {
|
|
|
28
28
|
repo: ArchiveServingRepo;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
type Logger = ReturnType<ComponentLogger['forComponent']>;
|
|
32
|
-
|
|
33
31
|
/**
|
|
34
32
|
* Service for handling incoming sync requests from other cluster peers.
|
|
35
33
|
*
|
|
@@ -53,7 +51,7 @@ export class SyncService implements Startable {
|
|
|
53
51
|
components: SyncServiceComponents,
|
|
54
52
|
init: SyncServiceInit = {}
|
|
55
53
|
) {
|
|
56
|
-
this.log =
|
|
54
|
+
this.log = createLogger('sync-service');
|
|
57
55
|
this.protocol = buildSyncProtocol(init.protocolPrefix ?? '');
|
|
58
56
|
this.repo = components.repo;
|
|
59
57
|
this.registrar = components.registrar;
|
|
@@ -485,6 +485,12 @@ export async function nonResponsibleNodes(mesh: Mesh, blockId: string): Promise<
|
|
|
485
485
|
export interface BuildTransactorOptions {
|
|
486
486
|
timeoutMs?: number;
|
|
487
487
|
abortOrCancelTimeoutMs?: number;
|
|
488
|
+
/**
|
|
489
|
+
* Wraps each node's repo before the transactor sees it, so a test can inject a transport-shaped
|
|
490
|
+
* failure into one RPC (a dropped sweep commit, a refused pend) and leave every other call — and
|
|
491
|
+
* therefore all of the production code after the injection — untouched.
|
|
492
|
+
*/
|
|
493
|
+
wrapRepo?: (repo: IRepo, node: MeshNode) => IRepo;
|
|
488
494
|
}
|
|
489
495
|
|
|
490
496
|
/**
|
|
@@ -496,7 +502,8 @@ export interface BuildTransactorOptions {
|
|
|
496
502
|
export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor => {
|
|
497
503
|
const repoByPeer = new Map<string, IRepo>();
|
|
498
504
|
for (const node of mesh.nodes) {
|
|
499
|
-
|
|
505
|
+
const repo = node.coordinatorRepo as unknown as IRepo;
|
|
506
|
+
repoByPeer.set(node.peerId.toString(), options.wrapRepo ? options.wrapRepo(repo, node) : repo);
|
|
500
507
|
}
|
|
501
508
|
return new NetworkTransactor({
|
|
502
509
|
timeoutMs: options.timeoutMs ?? 5_000,
|