@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.
Files changed (82) hide show
  1. package/dist/src/cluster/block-transfer-service.d.ts +0 -10
  2. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  3. package/dist/src/cluster/block-transfer-service.js +4 -2
  4. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  5. package/dist/src/cluster/cluster-policy.d.ts +101 -18
  6. package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
  7. package/dist/src/cluster/cluster-policy.js +153 -30
  8. package/dist/src/cluster/cluster-policy.js.map +1 -1
  9. package/dist/src/cluster/cluster-repo.d.ts +75 -16
  10. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  11. package/dist/src/cluster/cluster-repo.js +195 -24
  12. package/dist/src/cluster/cluster-repo.js.map +1 -1
  13. package/dist/src/cluster/quorum-restore.d.ts +4 -2
  14. package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
  15. package/dist/src/cluster/quorum-restore.js +4 -2
  16. package/dist/src/cluster/quorum-restore.js.map +1 -1
  17. package/dist/src/cluster/service.d.ts +12 -9
  18. package/dist/src/cluster/service.d.ts.map +1 -1
  19. package/dist/src/cluster/service.js +6 -6
  20. package/dist/src/cluster/service.js.map +1 -1
  21. package/dist/src/dispute/service.d.ts +1 -4
  22. package/dist/src/dispute/service.d.ts.map +1 -1
  23. package/dist/src/dispute/service.js +2 -1
  24. package/dist/src/dispute/service.js.map +1 -1
  25. package/dist/src/libp2p-key-network.d.ts +8 -2
  26. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  27. package/dist/src/libp2p-key-network.js +8 -2
  28. package/dist/src/libp2p-key-network.js.map +1 -1
  29. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  30. package/dist/src/libp2p-node-base.js +22 -19
  31. package/dist/src/libp2p-node-base.js.map +1 -1
  32. package/dist/src/logger.d.ts +28 -1
  33. package/dist/src/logger.d.ts.map +1 -1
  34. package/dist/src/logger.js +143 -1
  35. package/dist/src/logger.js.map +1 -1
  36. package/dist/src/network/network-manager-service.d.ts +1 -4
  37. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  38. package/dist/src/network/network-manager-service.js +2 -1
  39. package/dist/src/network/network-manager-service.js.map +1 -1
  40. package/dist/src/repo/cluster-coordinator.d.ts +16 -1
  41. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  42. package/dist/src/repo/cluster-coordinator.js +66 -7
  43. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  44. package/dist/src/repo/coordinator-repo.d.ts +188 -14
  45. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  46. package/dist/src/repo/coordinator-repo.js +714 -110
  47. package/dist/src/repo/coordinator-repo.js.map +1 -1
  48. package/dist/src/repo/service.d.ts +9 -6
  49. package/dist/src/repo/service.d.ts.map +1 -1
  50. package/dist/src/repo/service.js +4 -5
  51. package/dist/src/repo/service.js.map +1 -1
  52. package/dist/src/storage/storage-repo.d.ts +6 -0
  53. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  54. package/dist/src/storage/storage-repo.js +62 -2
  55. package/dist/src/storage/storage-repo.js.map +1 -1
  56. package/dist/src/sync/service.d.ts +1 -2
  57. package/dist/src/sync/service.d.ts.map +1 -1
  58. package/dist/src/sync/service.js +2 -1
  59. package/dist/src/sync/service.js.map +1 -1
  60. package/dist/src/testing/mesh-harness.d.ts +7 -1
  61. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  62. package/dist/src/testing/mesh-harness.js +2 -1
  63. package/dist/src/testing/mesh-harness.js.map +1 -1
  64. package/package.json +2 -2
  65. package/readme.md +19 -0
  66. package/src/cluster/block-transfer-service.ts +4 -8
  67. package/src/cluster/cluster-policy.ts +196 -36
  68. package/src/cluster/cluster-repo.ts +2662 -2488
  69. package/src/cluster/quorum-restore.ts +4 -2
  70. package/src/cluster/service.ts +14 -9
  71. package/src/dispute/service.ts +3 -3
  72. package/src/libp2p-key-network.ts +8 -2
  73. package/src/libp2p-node-base.ts +22 -19
  74. package/src/logger.ts +196 -2
  75. package/src/network/network-manager-service.ts +414 -414
  76. package/src/protocol-client.ts +196 -196
  77. package/src/repo/cluster-coordinator.ts +1113 -1039
  78. package/src/repo/coordinator-repo.ts +2675 -1937
  79. package/src/repo/service.ts +12 -9
  80. package/src/storage/storage-repo.ts +65 -2
  81. package/src/sync/service.ts +3 -5
  82. package/src/testing/mesh-harness.ts +8 -1
@@ -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, Logger, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
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 strand them under `db-p2p:repo-service:error`, outside the
98
- * `optimystic:db-p2p:*` tree every other address-book line lives in.
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 = components.logger.forComponent(init.logPrefix ?? 'db-p2p:repo-service')
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
- debugLog('redirect op=%s blockKey=%s cluster=%d', opName, blockKey, cluster.length)
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
- const collectionId = await this.internalCommit(blockId, request.actionId, request.rev, storage, latch, proof);
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
- private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage, latch: BlockWriteLatch, proof?: BlockCommitProof): Promise<CollectionId | undefined> {
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
@@ -1,4 +1,4 @@
1
- import type { ComponentLogger, Connection, Startable } from '@libp2p/interface';
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 = components.logger.forComponent('db-p2p:sync-service');
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
- repoByPeer.set(node.peerId.toString(), node.coordinatorRepo as unknown as IRepo);
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,