@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.
- package/dist/src/cluster/cluster-repo.d.ts +46 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +177 -101
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +7 -0
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +13 -6
- package/dist/src/logger.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +42 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +50 -9
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +74 -7
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +290 -55
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +15 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +28 -0
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +15 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +21 -4
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/src/cluster/cluster-repo.ts +2749 -2671
- package/src/libp2p-key-network.ts +7 -0
- package/src/logger.ts +14 -6
- package/src/repo/cluster-coordinator.ts +1170 -1113
- package/src/repo/coordinator-repo.ts +2940 -2687
- package/src/storage/storage-repo.ts +30 -0
- 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 =
|
|
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
|
-
|
|
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:
|