@optimystic/db-p2p 1.0.0-beta.2 → 1.0.0-beta.3

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 (34) hide show
  1. package/dist/src/cluster/cluster-repo.d.ts +46 -11
  2. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  3. package/dist/src/cluster/cluster-repo.js +177 -101
  4. package/dist/src/cluster/cluster-repo.js.map +1 -1
  5. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  6. package/dist/src/libp2p-key-network.js +7 -0
  7. package/dist/src/libp2p-key-network.js.map +1 -1
  8. package/dist/src/logger.d.ts.map +1 -1
  9. package/dist/src/logger.js +13 -6
  10. package/dist/src/logger.js.map +1 -1
  11. package/dist/src/repo/cluster-coordinator.d.ts +42 -0
  12. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  13. package/dist/src/repo/cluster-coordinator.js +50 -9
  14. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  15. package/dist/src/repo/coordinator-repo.d.ts +74 -7
  16. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  17. package/dist/src/repo/coordinator-repo.js +290 -55
  18. package/dist/src/repo/coordinator-repo.js.map +1 -1
  19. package/dist/src/storage/storage-repo.d.ts +15 -0
  20. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  21. package/dist/src/storage/storage-repo.js +28 -0
  22. package/dist/src/storage/storage-repo.js.map +1 -1
  23. package/dist/src/testing/mesh-harness.d.ts +15 -0
  24. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  25. package/dist/src/testing/mesh-harness.js +21 -4
  26. package/dist/src/testing/mesh-harness.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/cluster/cluster-repo.ts +2749 -2671
  29. package/src/libp2p-key-network.ts +7 -0
  30. package/src/logger.ts +14 -6
  31. package/src/repo/cluster-coordinator.ts +1170 -1113
  32. package/src/repo/coordinator-repo.ts +2940 -2687
  33. package/src/storage/storage-repo.ts +30 -0
  34. package/src/testing/mesh-harness.ts +37 -4
@@ -60,6 +60,25 @@ export function isMissingBaseRevisionFailure(result: CommitResult): boolean {
60
60
  return !result.success && (result.reason?.startsWith(MISSING_BASE_REVISION_REASON) ?? false);
61
61
  }
62
62
 
63
+ /**
64
+ * Stable, greppable prefix on the failure reason `CoordinatorRepo.commit` answers with when a commit
65
+ * assembled consensus but FEWER than a majority of the cohort reported durably holding the committed
66
+ * revision afterwards. Same convention as {@link MISSING_BASE_REVISION_REASON}: a string marker,
67
+ * because the reason crosses the wire as `StaleFailure.reason` prose. The refusal is retryable
68
+ * (`conflict: true`) and means "not confirmed durable at a quorum" — never "guaranteed absent"; see
69
+ * the durability gate in `CoordinatorRepo.commit` for the two-phase ambiguity that wording covers.
70
+ */
71
+ export const COMMIT_NOT_DURABLE_REASON = 'commit-not-durable';
72
+
73
+ /**
74
+ * True when a {@link CommitResult} was refused by the coordinator's durability gate — consensus was
75
+ * reached but no durable majority reported holding the revision. Sibling of
76
+ * {@link isMissingBaseRevisionFailure}, for callers that need to tell this refusal from a stale loss.
77
+ */
78
+ export function isCommitNotDurableFailure(result: CommitResult): boolean {
79
+ return !result.success && (result.reason?.startsWith(COMMIT_NOT_DURABLE_REASON) ?? false);
80
+ }
81
+
63
82
  export type StorageRepoOptions = {
64
83
  /** Optional hook to validate transactions in PendRequests */
65
84
  validatePend?: PendValidationHook;
@@ -630,6 +649,17 @@ export class StorageRepo implements IRepo, IBlockChangeNotifier, IBlockReplicaSt
630
649
  }
631
650
  }
632
651
  }
652
+ // NOTE: a pend of an update-only transform for a block this node holds NO revision of
653
+ // falls through here and is recorded (`latest` is undefined, so there is nothing to be
654
+ // stale against). It can never be promoted on this node without a reconcile —
655
+ // `internalCommit`'s fork guard refuses it (`missing-base-revision`) and drops the
656
+ // record — so the pend round it wins is one this member could not honour on its own.
657
+ // Harmless today: the commit-tier durability gate (`CoordinatorRepo.commit`) refuses
658
+ // the acknowledgement unless a majority of the cohort holds the revision after
659
+ // reconcile, and the coordinating member's proof-carrying copy is what a behind member
660
+ // reconciles from. If pend-time refusals ever become worth their cost (one wasted
661
+ // consensus round per such write), refuse at `ClusterMember.validatePendOperations`
662
+ // instead of here.
633
663
 
634
664
  // Then handle any pending actions
635
665
  const pending = await asyncIteratorToArray(blockStorage.listPendingTransactions());
@@ -4,7 +4,7 @@ import type { FindCoordinatorOptions } from '@optimystic/db-core';
4
4
  import type { IPeerNetwork } from '@optimystic/db-core';
5
5
  import { NetworkTransactor } from '@optimystic/db-core';
6
6
  import { peerIdFromPrivateKey } from '@libp2p/peer-id';
7
- import { generateKeyPair } from '@libp2p/crypto/keys';
7
+ import { generateKeyPair, generateKeyPairFromSeed } from '@libp2p/crypto/keys';
8
8
  import { ClusterMember, clusterMember, type ReconcileBlockCallback, type DeriveExpectedClusterCallback, type ExpectedClusterView } from '../cluster/cluster-repo.js';
9
9
  import { createReconcileBlock } from '../cluster/reconcile-block.js';
10
10
  import { resolveClusterPolicy, type ClusterPolicyOptions, type ResolvedClusterPolicy } from '../cluster/cluster-policy.js';
@@ -89,6 +89,21 @@ export interface MeshOptions {
89
89
  * than its threshold (0.5), so returning the threshold itself lands on the fail-closed side.
90
90
  */
91
91
  meshConfidence?: (node: MeshNode) => number;
92
+ /**
93
+ * Wraps the mesh's shared key network before any node, member derivation or transactor captures it —
94
+ * so a wrapper here observes EVERY cohort lookup in the mesh (each node's coordinator, cluster
95
+ * coordinator and admission derivation, plus `mesh.keyNetwork`), not only the transactor's.
96
+ * Reassigning `mesh.keyNetwork` after `createMesh` reaches the transactor alone.
97
+ * Omitted → identity.
98
+ */
99
+ wrapKeyNetwork?: (shared: IKeyNetwork) => IKeyNetwork;
100
+ /**
101
+ * Derive every node's Ed25519 key from a fixed seed `(keySeed, index)` instead of fresh
102
+ * randomness, so the mesh's ring geometry is identical on every run. For specs that assert on
103
+ * statistics of cohort placement: with random keys such a bound is a sample whose tail
104
+ * eventually crosses it. Omitted → random keys.
105
+ */
106
+ keySeed?: number;
92
107
  }
93
108
 
94
109
  export interface MeshFailureConfig {
@@ -238,6 +253,15 @@ export function resolveMeshPolicy(options: MeshOptions): ResolvedClusterPolicy {
238
253
  });
239
254
  }
240
255
 
256
+ /** The 32-byte Ed25519 seed for node `index` of a `keySeed` mesh. */
257
+ function meshKeySeed(keySeed: number, index: number): Uint8Array {
258
+ const seed = new Uint8Array(32);
259
+ const view = new DataView(seed.buffer);
260
+ view.setUint32(0, keySeed);
261
+ view.setUint32(4, index);
262
+ return seed;
263
+ }
264
+
241
265
  /**
242
266
  * Creates N interconnected mesh nodes with real components and mock transport.
243
267
  * ClusterClient calls route directly to target ClusterMember instances.
@@ -263,8 +287,10 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
263
287
 
264
288
  // Generate key pairs for all nodes
265
289
  const keyPairs = await Promise.all(
266
- Array.from({ length: nodeCount }, async () => {
267
- const privateKey = await generateKeyPair('Ed25519');
290
+ Array.from({ length: nodeCount }, async (_, i) => {
291
+ const privateKey = options.keySeed === undefined
292
+ ? await generateKeyPair('Ed25519')
293
+ : await generateKeyPairFromSeed('Ed25519', meshKeySeed(options.keySeed, i));
268
294
  return { peerId: peerIdFromPrivateKey(privateKey), privateKey };
269
295
  })
270
296
  );
@@ -283,7 +309,14 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
283
309
  // admission gate's view) needs a per-node key network in phase 1, and constructing it here beats
284
310
  // a late-bound slot a closure could fire on before it is filled. Safe because `nodes` is captured
285
311
  // by reference and only consulted at call time, after the array is fully populated.
286
- const keyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
312
+ //
313
+ // `wrapKeyNetwork` is applied HERE, before `makeNodeKeyNetwork` closes over `keyNetwork` below and
314
+ // before phase 1 builds any `deriveExpectedCluster` closure — every one of them reads the `keyNetwork`
315
+ // binding, so a wrapper assigned to it here is what every node's coordinator, cluster member and the
316
+ // returned `Mesh.keyNetwork` all observe. Reassigning `mesh.keyNetwork` after this function returns
317
+ // only reaches whoever reads that property later (the transactor); it is too late for the rest.
318
+ const sharedKeyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
319
+ const keyNetwork = options.wrapKeyNetwork ? options.wrapKeyNetwork(sharedKeyNetwork) : sharedKeyNetwork;
287
320
 
288
321
  /**
289
322
  * One node's own view of the key network — what `Libp2pKeyPeerNetwork` gives a real node: