@optimystic/db-p2p 1.0.0-beta.2 → 1.0.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 +73 -1
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +70 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +16 -1
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +73 -72
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts +4 -6
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +8 -13
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-policy.d.ts +2 -1
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +172 -17
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +553 -149
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.js +2 -2
- package/dist/src/cluster/race-resolution.d.ts +51 -7
- package/dist/src/cluster/race-resolution.d.ts.map +1 -1
- package/dist/src/cluster/race-resolution.js +71 -7
- package/dist/src/cluster/race-resolution.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +67 -5
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +120 -26
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -47
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -6
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/membership-source.js +1 -1
- package/dist/src/cohort-topic/membership-source.js.map +1 -1
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/peer-codec.js +5 -2
- package/dist/src/cohort-topic/peer-codec.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +24 -3
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +36 -3
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/cohort-topic/topic-router.d.ts +9 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -1
- package/dist/src/cohort-topic/topic-router.js +12 -2
- package/dist/src/cohort-topic/topic-router.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +15 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +78 -17
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +241 -147
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +35 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +151 -26
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/libp2p-node-rn.d.ts +2 -2
- package/dist/src/libp2p-node-rn.d.ts.map +1 -1
- package/dist/src/libp2p-node-rn.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +2 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -7
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +3 -4
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +9 -9
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +10 -5
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +11 -2
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/network/relay-reservation.d.ts +170 -0
- package/dist/src/network/relay-reservation.d.ts.map +1 -0
- package/dist/src/network/relay-reservation.js +527 -0
- package/dist/src/network/relay-reservation.js.map +1 -0
- package/dist/src/pend-validation.d.ts +3 -2
- package/dist/src/pend-validation.d.ts.map +1 -1
- package/dist/src/pend-validation.js +3 -2
- package/dist/src/pend-validation.js.map +1 -1
- package/dist/src/protocol-client.d.ts +8 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +11 -0
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +2 -1
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -1
- package/dist/src/reactivity/forwarder-host.js +2 -1
- package/dist/src/reactivity/forwarder-host.js.map +1 -1
- package/dist/src/reactivity/origination-manager.d.ts +3 -3
- package/dist/src/reactivity/origination-manager.js +3 -3
- package/dist/src/reactivity/recover-transport.d.ts +22 -14
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +32 -19
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/reactivity/subscription-manager.d.ts +2 -2
- package/dist/src/reactivity/topic-bytes.d.ts +6 -7
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -1
- package/dist/src/reactivity/topic-bytes.js +8 -9
- package/dist/src/reactivity/topic-bytes.js.map +1 -1
- package/dist/src/repo/client.d.ts +5 -2
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +49 -40
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +119 -5
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +181 -29
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +259 -76
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +716 -328
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/i-under-replication-ledger.d.ts +87 -0
- package/dist/src/repo/i-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/i-under-replication-ledger.js +2 -0
- package/dist/src/repo/i-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts +91 -0
- package/dist/src/repo/kv-under-replication-ledger.d.ts.map +1 -0
- package/dist/src/repo/kv-under-replication-ledger.js +287 -0
- package/dist/src/repo/kv-under-replication-ledger.js.map +1 -0
- package/dist/src/repo/responsibility.d.ts +37 -0
- package/dist/src/repo/responsibility.d.ts.map +1 -0
- package/dist/src/repo/responsibility.js +37 -0
- package/dist/src/repo/responsibility.js.map +1 -0
- package/dist/src/repo/service.d.ts +44 -15
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +64 -27
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/repo/stuck-reservation.d.ts +127 -0
- package/dist/src/repo/stuck-reservation.d.ts.map +1 -0
- package/dist/src/repo/stuck-reservation.js +179 -0
- package/dist/src/repo/stuck-reservation.js.map +1 -0
- package/dist/src/repo/under-replication-drain.d.ts +208 -0
- package/dist/src/repo/under-replication-drain.d.ts.map +1 -0
- package/dist/src/repo/under-replication-drain.js +461 -0
- package/dist/src/repo/under-replication-drain.js.map +1 -0
- package/dist/src/rn.d.ts +5 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +5 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-latch.d.ts +3 -2
- package/dist/src/storage/block-latch.d.ts.map +1 -1
- package/dist/src/storage/block-latch.js +7 -4
- package/dist/src/storage/block-latch.js.map +1 -1
- package/dist/src/storage/block-storage.d.ts +45 -3
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +156 -9
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +57 -5
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/pending-claim.d.ts +167 -0
- package/dist/src/storage/pending-claim.d.ts.map +1 -0
- package/dist/src/storage/pending-claim.js +156 -0
- package/dist/src/storage/pending-claim.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +13 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -1
- package/dist/src/storage/raw-store-codec.js +24 -3
- package/dist/src/storage/raw-store-codec.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator.js +2 -1
- package/dist/src/storage/restoration-coordinator.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.js +3 -3
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +137 -5
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +273 -68
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/struct.d.ts +58 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +92 -11
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +246 -103
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +27 -3
- package/src/cluster/block-transfer-service.ts +132 -4
- package/src/cluster/block-transfer.ts +79 -84
- package/src/cluster/client.ts +115 -119
- package/src/cluster/cluster-policy.ts +2 -1
- package/src/cluster/cluster-repo.ts +3115 -2671
- package/src/cluster/commit-proof.ts +2 -2
- package/src/cluster/race-resolution.ts +76 -7
- package/src/cluster/rebalance-monitor.ts +677 -539
- package/src/cluster/spread-on-churn.ts +34 -54
- package/src/cohort-topic/host.ts +8 -6
- package/src/cohort-topic/membership-source.ts +1 -1
- package/src/cohort-topic/peer-codec.ts +34 -31
- package/src/cohort-topic/stream-util.ts +39 -4
- package/src/cohort-topic/topic-router.ts +12 -2
- package/src/dispute/client.ts +17 -7
- package/src/index.ts +64 -59
- package/src/libp2p-key-network.ts +1377 -1253
- package/src/libp2p-node-base.ts +186 -27
- package/src/libp2p-node-rn.ts +31 -30
- package/src/libp2p-node.ts +37 -36
- package/src/logger.ts +20 -7
- package/src/matchmaking/query-transport.ts +9 -9
- package/src/network/network-manager-service.ts +16 -5
- package/src/network/relay-reservation.ts +636 -0
- package/src/pend-validation.ts +3 -2
- package/src/protocol-client.ts +13 -1
- package/src/reactivity/forwarder-host.ts +2 -1
- package/src/reactivity/origination-manager.ts +3 -3
- package/src/reactivity/recover-transport.ts +39 -22
- package/src/reactivity/subscription-manager.ts +2 -2
- package/src/reactivity/topic-bytes.ts +35 -37
- package/src/repo/client.ts +172 -173
- package/src/repo/cluster-coordinator.ts +1296 -1113
- package/src/repo/coordinator-repo.ts +3087 -2687
- package/src/repo/i-under-replication-ledger.ts +89 -0
- package/src/repo/kv-under-replication-ledger.ts +302 -0
- package/src/repo/responsibility.ts +48 -0
- package/src/repo/service.ts +76 -39
- package/src/repo/stuck-reservation.ts +224 -0
- package/src/repo/under-replication-drain.ts +582 -0
- package/src/rn.ts +74 -69
- package/src/storage/block-latch.ts +7 -4
- package/src/storage/block-storage.ts +161 -10
- package/src/storage/i-block-storage.ts +60 -5
- package/src/storage/pending-claim.ts +200 -0
- package/src/storage/raw-store-codec.ts +26 -3
- package/src/storage/restoration-coordinator.ts +212 -211
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/storage-repo.ts +311 -73
- package/src/storage/struct.ts +58 -0
- package/src/testing/mesh-harness.ts +329 -115
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
|
-
import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId,
|
|
2
|
+
import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId, ITransactor, ITransactionValidator, PeerId as DbPeerId, RoutingKey } from '@optimystic/db-core';
|
|
3
3
|
import type { FindCoordinatorOptions } from '@optimystic/db-core';
|
|
4
4
|
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
5
|
-
import { NetworkTransactor } from '@optimystic/db-core';
|
|
5
|
+
import { NetworkTransactor, routingKeyForBlock } from '@optimystic/db-core';
|
|
6
|
+
import { DigitreeStore, assembleCohort, hashKey, hashPeerId } from 'p2p-fret';
|
|
6
7
|
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
7
|
-
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
8
|
+
import { generateKeyPair, generateKeyPairFromSeed } from '@libp2p/crypto/keys';
|
|
8
9
|
import { ClusterMember, clusterMember, type ReconcileBlockCallback, type DeriveExpectedClusterCallback, type ExpectedClusterView } from '../cluster/cluster-repo.js';
|
|
9
10
|
import { createReconcileBlock } from '../cluster/reconcile-block.js';
|
|
10
11
|
import { resolveClusterPolicy, type ClusterPolicyOptions, type ResolvedClusterPolicy } from '../cluster/cluster-policy.js';
|
|
@@ -15,13 +16,19 @@ import type { IRawStorage } from '../storage/i-raw-storage.js';
|
|
|
15
16
|
import type { BlockArchive } from '../storage/struct.js';
|
|
16
17
|
import { serveBlockArchive, servableProof } from '../storage/block-archive.js';
|
|
17
18
|
import { coordinatorRepo, type ClusterLatestCallback, type CertifiedActionRev } from '../repo/coordinator-repo.js';
|
|
19
|
+
import type { CommittedHolders } from '../cluster/rebalance-monitor.js';
|
|
18
20
|
import type { CoordinatorRepo } from '../repo/coordinator-repo.js';
|
|
19
|
-
import { sortPeersByDistance, type KnownPeer } from '../routing/responsibility.js';
|
|
20
21
|
import { toString as u8ToString } from 'uint8arrays';
|
|
21
22
|
|
|
22
23
|
export interface MeshNode {
|
|
23
24
|
peerId: PeerId;
|
|
24
25
|
privateKey: PrivateKey;
|
|
26
|
+
/**
|
|
27
|
+
* This node's own view of the key network — the harness analogue of a real node's `keyNetwork`
|
|
28
|
+
* attachment, and the instance its coordinator and its member's admission derivation both read. It
|
|
29
|
+
* differs from `Mesh.keyNetwork` only under a simulated partition (`MeshFailureConfig.partitionSides`).
|
|
30
|
+
*/
|
|
31
|
+
keyNetwork: IKeyNetwork;
|
|
25
32
|
storageRepo: StorageRepo;
|
|
26
33
|
clusterMember: ClusterMember;
|
|
27
34
|
coordinatorRepo: CoordinatorRepo;
|
|
@@ -58,9 +65,9 @@ export interface MeshOptions {
|
|
|
58
65
|
/**
|
|
59
66
|
* Per-node member-side cluster derivation for the membership admission gate — the harness
|
|
60
67
|
* analogue of `libp2p-node-base`'s `deriveExpectedCluster` (findCluster + FRET confidence).
|
|
61
|
-
* Omitted → each member gets the production-shaped derivation over its own
|
|
62
|
-
*
|
|
63
|
-
*
|
|
68
|
+
* Omitted → each member gets the production-shaped derivation over its own key-network view
|
|
69
|
+
* (partition-aware, see `MeshFailureConfig.partitionSides`) with confidence from `meshConfidence`
|
|
70
|
+
* (default 1).
|
|
64
71
|
*/
|
|
65
72
|
deriveExpectedCluster?: (node: MeshNode, blockId: BlockId) => Promise<ExpectedClusterView>;
|
|
66
73
|
/**
|
|
@@ -79,7 +86,8 @@ export interface MeshOptions {
|
|
|
79
86
|
*
|
|
80
87
|
* A mesh that arms validators can also set `clusterPolicy.unvalidatablePendPolicy: 'reject'` to
|
|
81
88
|
* refuse pends that carry no `validation` payload (the single-collection `Collection.sync`
|
|
82
|
-
* shape,
|
|
89
|
+
* shape, and the Quereus adapter's legacy multi-tree commit — neither can be re-checked); the
|
|
90
|
+
* default 'accept' admits them unchecked.
|
|
83
91
|
*/
|
|
84
92
|
validatorFactory?: (index: number, peerId: PeerId) => ITransactionValidator;
|
|
85
93
|
/**
|
|
@@ -89,6 +97,28 @@ export interface MeshOptions {
|
|
|
89
97
|
* than its threshold (0.5), so returning the threshold itself lands on the fail-closed side.
|
|
90
98
|
*/
|
|
91
99
|
meshConfidence?: (node: MeshNode) => number;
|
|
100
|
+
/**
|
|
101
|
+
* Wraps the mesh's shared key network before any node, member derivation or transactor captures it —
|
|
102
|
+
* so a wrapper here observes EVERY cohort lookup in the mesh (each node's coordinator, cluster
|
|
103
|
+
* coordinator and admission derivation, plus `mesh.keyNetwork`), not only the transactor's.
|
|
104
|
+
* Reassigning `mesh.keyNetwork` after `createMesh` reaches the transactor alone.
|
|
105
|
+
* Omitted → identity.
|
|
106
|
+
*/
|
|
107
|
+
wrapKeyNetwork?: (shared: IKeyNetwork) => IKeyNetwork;
|
|
108
|
+
/**
|
|
109
|
+
* Derive every node's Ed25519 key from a fixed seed `(keySeed, index)` instead of fresh
|
|
110
|
+
* randomness, so the mesh's ring geometry is identical on every run. For specs that assert on
|
|
111
|
+
* statistics of cohort placement: with random keys such a bound is a sample whose tail
|
|
112
|
+
* eventually crosses it. Omitted → random keys.
|
|
113
|
+
*/
|
|
114
|
+
keySeed?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Receives each node's committed-holders reports — from its member after a durable consensus apply
|
|
117
|
+
* and from its coordinator on acknowledging a cohort commit — the harness analogue of the sink
|
|
118
|
+
* `libp2p-node-base` routes to the node's rebalance monitor. The harness builds no monitor; a spec
|
|
119
|
+
* that wants one routes reports to its own. Omitted → nothing is reported.
|
|
120
|
+
*/
|
|
121
|
+
onCommittedHolders?: (node: MeshNode, committed: CommittedHolders) => void;
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
export interface MeshFailureConfig {
|
|
@@ -116,6 +146,18 @@ export interface MeshFailureConfig {
|
|
|
116
146
|
* it declared, which is already its own side.
|
|
117
147
|
*/
|
|
118
148
|
partitionSides?: Set<string>[];
|
|
149
|
+
/**
|
|
150
|
+
* Observes every REMOTE cluster delivery — promise round, commit round, commit broadcast and
|
|
151
|
+
* scheduled commit retry alike — with the target's peer-id string and the record as sent, BEFORE
|
|
152
|
+
* `failingPeers` is consulted for that delivery. It exists so a spec can change reachability at an
|
|
153
|
+
* exact protocol step rather than between whole transactions: a member that promised and then
|
|
154
|
+
* dropped before voting to commit is set unreachable here, on the delivery that would have carried
|
|
155
|
+
* its commit vote, and that very delivery fails.
|
|
156
|
+
*
|
|
157
|
+
* A coordinator's OWN member is invoked in process, never through this path — as in production,
|
|
158
|
+
* where `ClusterCoordinator.updateMember` calls it directly — so the hook never sees it.
|
|
159
|
+
*/
|
|
160
|
+
onClusterDelivery?: (targetPeerId: string, record: ClusterRecord) => void;
|
|
119
161
|
}
|
|
120
162
|
|
|
121
163
|
class MockPeerNetwork implements IPeerNetwork {
|
|
@@ -125,38 +167,46 @@ class MockPeerNetwork implements IPeerNetwork {
|
|
|
125
167
|
}
|
|
126
168
|
|
|
127
169
|
/**
|
|
128
|
-
* Mock IKeyNetwork that
|
|
129
|
-
*
|
|
130
|
-
*
|
|
170
|
+
* Mock IKeyNetwork that places a block where production does, by construction: it ranks the mesh's nodes
|
|
171
|
+
* with FRET's own `assembleCohort` over a ring (`DigitreeStore`) holding every node at its real ring
|
|
172
|
+
* coordinate (`hashPeerId`), the walk `Libp2pKeyPeerNetwork` asks FRET for. Like production it hashes the
|
|
173
|
+
* routing key exactly once. Every mesh node serves, so the network-membership scoping production layers on
|
|
174
|
+
* top never removes anyone, and a cohort is simply the nearest `responsibilityK` in walk order.
|
|
175
|
+
* `test/mesh-harness-cohort-parity.spec.ts` pins the two answers equal.
|
|
176
|
+
*
|
|
177
|
+
* - `findCluster`: the nearest `responsibilityK` nodes, nearest first. Nothing is added: a node is in a
|
|
178
|
+
* block's cohort only when it is among the nearest, as in production.
|
|
179
|
+
* - `findCoordinator`: the first non-excluded node of the whole ring's walk, which is the cohort's first
|
|
180
|
+
* member unless excluded, else the next nearest node, cohort member or not. That mirrors production's
|
|
181
|
+
* connected-peer fallback: a pick outside the cohort is a routing hop, not a placement.
|
|
131
182
|
*/
|
|
132
183
|
class MockMeshKeyNetwork implements IKeyNetwork {
|
|
184
|
+
/** The ring, built on first lookup (node ids hash asynchronously, and `nodes` fills after construction)
|
|
185
|
+
* and rebuilt only if the node count changes; a restarted node keeps its identity and so its position. */
|
|
186
|
+
private ring: { size: number; ready: Promise<{ store: DigitreeStore; byId: Map<string, MeshNode> }> } | undefined;
|
|
187
|
+
|
|
133
188
|
constructor(
|
|
134
189
|
private readonly nodes: MeshNode[],
|
|
135
190
|
private readonly responsibilityK: number,
|
|
136
191
|
private readonly failures: MeshFailureConfig = {}
|
|
137
192
|
) {}
|
|
138
193
|
|
|
139
|
-
async findCoordinator(key:
|
|
194
|
+
async findCoordinator(key: RoutingKey, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
|
|
140
195
|
const excluded = new Set((options?.excludedPeers ?? []).map(p => p.toString()));
|
|
141
|
-
const
|
|
142
|
-
const pick = sorted.find(n => !excluded.has(n.peerId.toString()));
|
|
196
|
+
const pick = (await this.nearest(key, this.nodes.length)).find(n => !excluded.has(n.peerId.toString()));
|
|
143
197
|
if (!pick) {
|
|
144
198
|
throw new Error('No coordinator available for key (all candidates excluded)');
|
|
145
199
|
}
|
|
146
200
|
return pick.peerId;
|
|
147
201
|
}
|
|
148
202
|
|
|
149
|
-
async findCluster(key:
|
|
203
|
+
async findCluster(key: RoutingKey): Promise<ClusterPeers> {
|
|
150
204
|
if (this.failures.findClusterFails) {
|
|
151
205
|
return {} as ClusterPeers;
|
|
152
206
|
}
|
|
153
207
|
|
|
154
|
-
const sorted = this.sortedByDistance(key);
|
|
155
|
-
const k = Math.min(this.responsibilityK, sorted.length);
|
|
156
|
-
const selected = sorted.slice(0, k);
|
|
157
|
-
|
|
158
208
|
const peers: ClusterPeers = {};
|
|
159
|
-
for (const node of
|
|
209
|
+
for (const node of await this.nearest(key, this.responsibilityK)) {
|
|
160
210
|
peers[node.peerId.toString()] = {
|
|
161
211
|
multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
|
|
162
212
|
publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
|
|
@@ -165,13 +215,30 @@ class MockMeshKeyNetwork implements IKeyNetwork {
|
|
|
165
215
|
return peers;
|
|
166
216
|
}
|
|
167
217
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
218
|
+
/** The `wants` nodes nearest `key`'s ring coordinate, nearest first. */
|
|
219
|
+
private async nearest(key: RoutingKey, wants: number): Promise<MeshNode[]> {
|
|
220
|
+
const { store, byId } = await this.ringStore();
|
|
221
|
+
const ids = assembleCohort(store, await hashKey(key), Math.min(wants, byId.size));
|
|
222
|
+
return ids.map(id => byId.get(id)!);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private ringStore(): Promise<{ store: DigitreeStore; byId: Map<string, MeshNode> }> {
|
|
226
|
+
if (this.ring?.size !== this.nodes.length) {
|
|
227
|
+
const members = [...this.nodes];
|
|
228
|
+
this.ring = {
|
|
229
|
+
size: members.length,
|
|
230
|
+
ready: (async () => {
|
|
231
|
+
const store = new DigitreeStore();
|
|
232
|
+
const byId = new Map<string, MeshNode>();
|
|
233
|
+
for (const node of members) {
|
|
234
|
+
store.upsert(node.peerId.toString(), await hashPeerId(node.peerId));
|
|
235
|
+
byId.set(node.peerId.toString(), node);
|
|
236
|
+
}
|
|
237
|
+
return { store, byId };
|
|
238
|
+
})()
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return this.ring.ready;
|
|
175
242
|
}
|
|
176
243
|
}
|
|
177
244
|
|
|
@@ -179,6 +246,25 @@ export interface Mesh {
|
|
|
179
246
|
nodes: MeshNode[];
|
|
180
247
|
failures: MeshFailureConfig;
|
|
181
248
|
keyNetwork: IKeyNetwork;
|
|
249
|
+
/**
|
|
250
|
+
* Restart one node the way a stopped and relaunched process comes back: same identity, same raw
|
|
251
|
+
* storage, and nothing else. `node`'s storage repo, cluster member and coordinator are rebuilt IN
|
|
252
|
+
* PLACE on the same `MeshNode` object (so a spec's reference to it stays valid), over the
|
|
253
|
+
* `IRawStorage` instance it was built with — for the default `MemoryRawStorage` that is exactly
|
|
254
|
+
* "over its own storage"; a `rawStorageFactory` store is reused as-is, never re-requested.
|
|
255
|
+
*
|
|
256
|
+
* Everything held only in memory goes with the old instance: the member's reservations and
|
|
257
|
+
* executed-transaction memory, the coordinator's in-flight transactions and scheduled commit
|
|
258
|
+
* retries, every read-repair stamp. The harness wires no `ITransactionStateStore`, so nothing is
|
|
259
|
+
* recovered either — a production node built with `transactionStateStore` would resume its
|
|
260
|
+
* persisted commit retries on restart (`ClusterCoordinator.recoverTransactions`). The old instance is made inert rather than merely forgotten —
|
|
261
|
+
* its member is disposed, and every outbound call it would still make (a commit retry whose timer
|
|
262
|
+
* fires later, a cohort consult, an archive fetch) fails the way a stopped process's dial does — so
|
|
263
|
+
* a retry the old process owed cannot quietly deliver after the restart and pass for healing.
|
|
264
|
+
*
|
|
265
|
+
* Reachability (`failures`) is untouched: a node that was unreachable stays unreachable.
|
|
266
|
+
*/
|
|
267
|
+
restart(node: MeshNode): void;
|
|
182
268
|
}
|
|
183
269
|
|
|
184
270
|
/**
|
|
@@ -238,6 +324,21 @@ export function resolveMeshPolicy(options: MeshOptions): ResolvedClusterPolicy {
|
|
|
238
324
|
});
|
|
239
325
|
}
|
|
240
326
|
|
|
327
|
+
/** Whether one node INSTANCE is still running. Every outbound call the instance makes checks it, so an
|
|
328
|
+
* instance {@link Mesh.restart} replaced goes quiet instead of acting alongside the new one. */
|
|
329
|
+
interface NodeLifetime {
|
|
330
|
+
stopped: boolean;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The 32-byte Ed25519 seed for node `index` of a `keySeed` mesh. */
|
|
334
|
+
function meshKeySeed(keySeed: number, index: number): Uint8Array {
|
|
335
|
+
const seed = new Uint8Array(32);
|
|
336
|
+
const view = new DataView(seed.buffer);
|
|
337
|
+
view.setUint32(0, keySeed);
|
|
338
|
+
view.setUint32(4, index);
|
|
339
|
+
return seed;
|
|
340
|
+
}
|
|
341
|
+
|
|
241
342
|
/**
|
|
242
343
|
* Creates N interconnected mesh nodes with real components and mock transport.
|
|
243
344
|
* ClusterClient calls route directly to target ClusterMember instances.
|
|
@@ -263,37 +364,55 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
263
364
|
|
|
264
365
|
// Generate key pairs for all nodes
|
|
265
366
|
const keyPairs = await Promise.all(
|
|
266
|
-
Array.from({ length: nodeCount }, async () => {
|
|
267
|
-
const privateKey =
|
|
367
|
+
Array.from({ length: nodeCount }, async (_, i) => {
|
|
368
|
+
const privateKey = options.keySeed === undefined
|
|
369
|
+
? await generateKeyPair('Ed25519')
|
|
370
|
+
: await generateKeyPairFromSeed('Ed25519', meshKeySeed(options.keySeed, i));
|
|
268
371
|
return { peerId: peerIdFromPrivateKey(privateKey), privateKey };
|
|
269
372
|
})
|
|
270
373
|
);
|
|
271
374
|
|
|
272
|
-
// Build nodes array (partially — coordinatorRepo added after keyNetwork is ready)
|
|
273
375
|
const nodes: MeshNode[] = [];
|
|
274
376
|
const peerNetwork = new MockPeerNetwork();
|
|
275
377
|
// One real reconcile callback per node, shared between the member's commit-path `reconcileBlock`
|
|
276
378
|
// and the coordinator's read-path `acquireBlockFromCohort` — production shares one instance
|
|
277
|
-
// (`libp2p-node-base.ts`), and a spec must not be able to tell the two paths apart. Built
|
|
278
|
-
//
|
|
279
|
-
//
|
|
379
|
+
// (`libp2p-node-base.ts`), and a spec must not be able to tell the two paths apart. Built with the
|
|
380
|
+
// member, consumed again by the coordinator, so it is stashed here keyed by peer id rather than
|
|
381
|
+
// widened onto the public `MeshNode` type.
|
|
280
382
|
const reconcileByPeer = new Map<string, ReconcileBlockCallback>();
|
|
383
|
+
/** What outlives a node instance, by peer id: the raw storage it was built over, and its index
|
|
384
|
+
* (`validatorFactory` is invoked with it again on restart). */
|
|
385
|
+
const durableByPeer = new Map<string, { rawStorage: IRawStorage; index: number }>();
|
|
386
|
+
/** The CURRENT instance's lifetime, by peer id — flipped to stopped by {@link Mesh.restart}. */
|
|
387
|
+
const lifetimeByPeer = new Map<string, NodeLifetime>();
|
|
281
388
|
|
|
282
389
|
// The mesh key network is built BEFORE the members: each member's `deriveExpectedCluster` (the
|
|
283
|
-
// admission gate's view) needs a per-node key network
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
390
|
+
// admission gate's view) needs a per-node key network, and constructing it here beats a late-bound
|
|
391
|
+
// slot a closure could fire on before it is filled. Safe because `nodes` is captured by reference
|
|
392
|
+
// and only consulted at call time, after the array is fully populated.
|
|
393
|
+
//
|
|
394
|
+
// `wrapKeyNetwork` is applied HERE, before `makeNodeKeyNetwork` closes over `keyNetwork` below and
|
|
395
|
+
// before any `deriveExpectedCluster` closure is built — every one of them reads the `keyNetwork`
|
|
396
|
+
// binding, so a wrapper assigned to it here is what every node's coordinator, cluster member and the
|
|
397
|
+
// returned `Mesh.keyNetwork` all observe. Reassigning `mesh.keyNetwork` after this function returns
|
|
398
|
+
// only reaches whoever reads that property later (the transactor); it is too late for the rest.
|
|
399
|
+
const sharedKeyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
|
|
400
|
+
const keyNetwork = options.wrapKeyNetwork ? options.wrapKeyNetwork(sharedKeyNetwork) : sharedKeyNetwork;
|
|
287
401
|
|
|
288
402
|
/**
|
|
289
403
|
* One node's own view of the key network — what `Libp2pKeyPeerNetwork` gives a real node:
|
|
290
|
-
* - `findCluster`
|
|
291
|
-
*
|
|
404
|
+
* - `findCluster` is the shared cohort, which holds this node only when it is among the block's
|
|
405
|
+
* nearest `responsibilityK`, so the node's coordinator judges its own responsibility as a real
|
|
406
|
+
* node's does;
|
|
292
407
|
* - under a simulated partition (`failures.partitionSides`), a caller inside a side sees only
|
|
293
408
|
* its side's members of the cohort — the caller-aware filtering lives here, in the per-node
|
|
294
|
-
* wrapper, precisely so `IKeyNetwork` itself needs no "who is asking" parameter.
|
|
295
|
-
*
|
|
296
|
-
*
|
|
409
|
+
* wrapper, precisely so `IKeyNetwork` itself needs no "who is asking" parameter. A side holding
|
|
410
|
+
* none of the cohort sees an empty view, so a node there is not responsible for the block (and
|
|
411
|
+
* the empty-view guard in `cluster-repo.admitMembership` treats such a view as unconfident rather
|
|
412
|
+
* than as a reference set).
|
|
413
|
+
* The SAME instance serves both the member's admission derivation and the node's coordinator, so
|
|
414
|
+
* the two sides of a node can never see different topologies. It holds no state, so a restarted
|
|
415
|
+
* node keeps it.
|
|
297
416
|
*/
|
|
298
417
|
const makeNodeKeyNetwork = (selfPeerId: PeerId): IKeyNetwork => {
|
|
299
418
|
const selfStr = selfPeerId.toString();
|
|
@@ -307,25 +426,18 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
307
426
|
if (!side.has(id)) delete peers[id];
|
|
308
427
|
}
|
|
309
428
|
}
|
|
310
|
-
if (!(selfStr in peers)) {
|
|
311
|
-
peers[selfStr] = {
|
|
312
|
-
multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
|
|
313
|
-
publicKey: u8ToString(selfPeerId.publicKey!.raw, 'base64url')
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
429
|
return peers;
|
|
317
430
|
}
|
|
318
431
|
};
|
|
319
432
|
};
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
: new MemoryRawStorage();
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Build `meshNode`'s storage repo and cluster member over its durable raw storage, assigning both
|
|
436
|
+
* onto the node. Run once per node at assembly, and again by {@link Mesh.restart}.
|
|
437
|
+
*/
|
|
438
|
+
const buildMember = (meshNode: MeshNode, lifetime: NodeLifetime): void => {
|
|
439
|
+
const { peerId, privateKey } = meshNode;
|
|
440
|
+
const { rawStorage, index } = durableByPeer.get(peerId.toString())!;
|
|
329
441
|
const storageRepo = new StorageRepo(
|
|
330
442
|
(blockId: BlockId) => new BlockStorage(blockId, rawStorage)
|
|
331
443
|
);
|
|
@@ -336,9 +448,12 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
336
448
|
// distinct peers must agree on the target `(rev, actionId)` AND on the block content, or the
|
|
337
449
|
// pass declines, persisting nothing. `reputation` is omitted — no reputation subsystem in the
|
|
338
450
|
// harness.
|
|
451
|
+
const fetchArchive = makeFetchArchive(nodes, peerId.toString(), failures);
|
|
339
452
|
const reconcileBlock = createReconcileBlock({
|
|
340
453
|
selfPeerId: peerId.toString(),
|
|
341
|
-
|
|
454
|
+
// A stopped instance fetches nothing — the production fetch swallows a failed dial into
|
|
455
|
+
// the same `undefined`.
|
|
456
|
+
fetchArchive: async (peerIdStr, blockId) => lifetime.stopped ? undefined : await fetchArchive(peerIdStr, blockId),
|
|
342
457
|
// Production shape: a proof reconcile verified against the agreed bytes is persisted so
|
|
343
458
|
// the repaired replica serves it onward.
|
|
344
459
|
saveReplicatedBlock: (blockId, block, source, verifiedProof) =>
|
|
@@ -349,33 +464,21 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
349
464
|
});
|
|
350
465
|
reconcileByPeer.set(peerId.toString(), reconcileBlock);
|
|
351
466
|
|
|
352
|
-
const nodeKeyNetwork =
|
|
353
|
-
nodeKeyNetworkByPeer.set(peerId.toString(), nodeKeyNetwork);
|
|
354
|
-
|
|
355
|
-
// The node object exists before its member so the admission derivation below can hand the
|
|
356
|
-
// finished MeshNode to spec-supplied callbacks; `clusterMember`/`coordinatorRepo` are
|
|
357
|
-
// assigned as they are built (member just below, coordinator in phase 2) and the closures
|
|
358
|
-
// only run at vote time, long after both are in place.
|
|
359
|
-
const meshNode: MeshNode = {
|
|
360
|
-
peerId,
|
|
361
|
-
privateKey,
|
|
362
|
-
storageRepo,
|
|
363
|
-
clusterMember: undefined as any,
|
|
364
|
-
coordinatorRepo: undefined as any
|
|
365
|
-
};
|
|
467
|
+
const nodeKeyNetwork = meshNode.keyNetwork;
|
|
366
468
|
|
|
367
469
|
// Member-side cluster derivation for the membership admission gate — the production shape
|
|
368
470
|
// (`libp2p-node-base.deriveExpectedCluster`): the SAME per-node key network the coordinator
|
|
369
|
-
// selects its cohort from, plus a network-size confidence.
|
|
370
|
-
//
|
|
471
|
+
// selects its cohort from, plus a network-size confidence. A responsible member is in its own
|
|
472
|
+
// cohort, so its view is never empty; `meshConfidence` is the FRET stand-in (default 1, i.e.
|
|
371
473
|
// confident — a partition spec collapses it per side).
|
|
372
474
|
const deriveExpectedCluster: DeriveExpectedClusterCallback = options.deriveExpectedCluster
|
|
373
475
|
? (blockId) => options.deriveExpectedCluster!(meshNode, blockId)
|
|
374
476
|
: async (blockId) => ({
|
|
375
|
-
peers: await nodeKeyNetwork.findCluster(
|
|
477
|
+
peers: await nodeKeyNetwork.findCluster(routingKeyForBlock(blockId)) ?? {},
|
|
376
478
|
confidence: options.meshConfidence?.(meshNode) ?? 1
|
|
377
479
|
});
|
|
378
480
|
|
|
481
|
+
meshNode.storageRepo = storageRepo;
|
|
379
482
|
meshNode.clusterMember = clusterMember({
|
|
380
483
|
storageRepo,
|
|
381
484
|
peerNetwork,
|
|
@@ -386,13 +489,13 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
386
489
|
deriveExpectedCluster,
|
|
387
490
|
// Absent by default: `undefined` here is identical to omitting the field, and
|
|
388
491
|
// `validatePendOperations` then skips the validation step entirely.
|
|
389
|
-
validator: options.validatorFactory?.(index, peerId)
|
|
492
|
+
validator: options.validatorFactory?.(index, peerId),
|
|
493
|
+
onCommittedHolders: options.onCommittedHolders && ((committed) => options.onCommittedHolders!(meshNode, committed))
|
|
390
494
|
});
|
|
495
|
+
};
|
|
391
496
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Phase 2: coordinator repos (needs all nodes for routing; key network built in phase 1)
|
|
497
|
+
// Cluster traffic to a node, resolved per call: a delivery always reaches the target's CURRENT
|
|
498
|
+
// member, so a restarted node answers with its new instance.
|
|
396
499
|
const createClusterClient = (targetPeerId: PeerId): ICluster => {
|
|
397
500
|
const target = nodes.find(n => n.peerId.equals(targetPeerId));
|
|
398
501
|
if (!target) {
|
|
@@ -400,6 +503,7 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
400
503
|
}
|
|
401
504
|
return {
|
|
402
505
|
async update(record: ClusterRecord): Promise<ClusterRecord> {
|
|
506
|
+
failures.onClusterDelivery?.(targetPeerId.toString(), record);
|
|
403
507
|
if (failures.failingPeers?.has(targetPeerId.toString())) {
|
|
404
508
|
throw new Error(`Peer ${targetPeerId.toString()} is unreachable`);
|
|
405
509
|
}
|
|
@@ -408,7 +512,13 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
408
512
|
};
|
|
409
513
|
};
|
|
410
514
|
|
|
411
|
-
|
|
515
|
+
/**
|
|
516
|
+
* Build `node`'s coordinator over its (already built) storage repo and member. It reaches the other
|
|
517
|
+
* nodes only at call time, never at construction. Run once per node at assembly, and again by
|
|
518
|
+
* {@link Mesh.restart}.
|
|
519
|
+
*/
|
|
520
|
+
const buildCoordinator = (node: MeshNode, lifetime: NodeLifetime): void => {
|
|
521
|
+
const stoppedError = (): Error => new Error(`${node.peerId.toString()} was restarted; this instance is stopped`);
|
|
412
522
|
// Per-node callback: reports the queried peer's latest revision, and NOTHING else. It used to
|
|
413
523
|
// also write the peer's block into local storage ("simulate data sync"), which made every
|
|
414
524
|
// read-repair assertion on this harness observe a convergence the production callback does not
|
|
@@ -416,6 +526,7 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
416
526
|
// existed to expose. Transfer now happens where it does in production: through
|
|
417
527
|
// `acquireBlockFromCohort` below, gated on a corroborated revision.
|
|
418
528
|
const clusterLatestCallback: ClusterLatestCallback = async (peerId: PeerId, blockId: BlockId, context?): Promise<CertifiedActionRev | undefined> => {
|
|
529
|
+
if (lifetime.stopped) throw stoppedError();
|
|
419
530
|
// Silence: the peer never answers. REJECTS, mirroring what a dial failure does to the
|
|
420
531
|
// production callback — the coordinator must count this as "did not answer", never as
|
|
421
532
|
// an absent claim (a resolved `undefined` remains the peer answering "I hold nothing").
|
|
@@ -439,19 +550,29 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
439
550
|
const proof = await servableProof(target.storageRepo, blockId, latest);
|
|
440
551
|
return proof ? { ...latest, proof } : latest;
|
|
441
552
|
};
|
|
442
|
-
// The node's own
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
const nodeKeyNetwork = nodeKeyNetworkByPeer.get(node.peerId.toString())!;
|
|
553
|
+
// The node's own (partition-aware) key-network view — the SAME instance the member's admission
|
|
554
|
+
// derivation reads, matching real Libp2pKeyPeerNetwork behavior.
|
|
555
|
+
const nodeKeyNetwork = node.keyNetwork;
|
|
446
556
|
const factory = coordinatorRepo(
|
|
447
557
|
nodeKeyNetwork,
|
|
448
|
-
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
558
|
+
// A stopped instance's cluster traffic fails at send — checked per delivery rather than when
|
|
559
|
+
// the client is made, because `ClusterCoordinator` also sends from a commit-retry timer that
|
|
560
|
+
// can fire long after a restart.
|
|
561
|
+
(targetPeerId: PeerId): ICluster => {
|
|
562
|
+
const client = createClusterClient(targetPeerId);
|
|
563
|
+
return {
|
|
564
|
+
async update(record: ClusterRecord): Promise<ClusterRecord> {
|
|
565
|
+
if (lifetime.stopped) throw stoppedError();
|
|
566
|
+
return await client.update(record);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
// The SAME resolved policy the member was built from, spread the way `libp2p-node-base`
|
|
571
|
+
// spreads it into its coordinator factory — carrying `repairCorroborationClusterSize` (the
|
|
572
|
+
// repair floor's yardstick, DEFAULT_CLUSTER_SIZE when the mesh declared nothing), the
|
|
573
|
+
// production `minAbsoluteClusterSize` (2, not the coordinator's own fallback of 3), and the
|
|
574
|
+
// `allowUnvalidatedSmallCluster` gate — ARMED (false) unless the mesh opted out at its call
|
|
575
|
+
// site.
|
|
455
576
|
{ ...policy }
|
|
456
577
|
);
|
|
457
578
|
node.coordinatorRepo = factory({
|
|
@@ -461,27 +582,110 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
461
582
|
clusterLatestCallback,
|
|
462
583
|
// The read path's transfer mechanism — the SAME instance the member uses on the commit
|
|
463
584
|
// path, mirroring how `libp2p-node-base` shares one `reconcileBlock` between both.
|
|
464
|
-
acquireBlockFromCohort: reconcileByPeer.get(node.peerId.toString())
|
|
585
|
+
acquireBlockFromCohort: reconcileByPeer.get(node.peerId.toString())!,
|
|
586
|
+
onCommittedHolders: options.onCommittedHolders && ((committed) => options.onCommittedHolders!(node, committed))
|
|
587
|
+
});
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// Phase 1: storage + cluster members, in key-pair order (the order `validatorFactory` indexes by).
|
|
591
|
+
for (const [index, { peerId, privateKey }] of keyPairs.entries()) {
|
|
592
|
+
const peerIdStr = peerId.toString();
|
|
593
|
+
durableByPeer.set(peerIdStr, {
|
|
594
|
+
rawStorage: options.rawStorageFactory ? options.rawStorageFactory(index) : new MemoryRawStorage(),
|
|
595
|
+
index
|
|
465
596
|
});
|
|
597
|
+
const lifetime: NodeLifetime = { stopped: false };
|
|
598
|
+
lifetimeByPeer.set(peerIdStr, lifetime);
|
|
599
|
+
|
|
600
|
+
// The node object exists before its member so the admission derivation can hand the finished
|
|
601
|
+
// MeshNode to spec-supplied callbacks; `storageRepo`/`clusterMember` are assigned by
|
|
602
|
+
// `buildMember` just below and `coordinatorRepo` in phase 2, and those closures only run at
|
|
603
|
+
// vote time, long after all three are in place.
|
|
604
|
+
const meshNode: MeshNode = {
|
|
605
|
+
peerId,
|
|
606
|
+
privateKey,
|
|
607
|
+
keyNetwork: makeNodeKeyNetwork(peerId),
|
|
608
|
+
storageRepo: undefined as any,
|
|
609
|
+
clusterMember: undefined as any,
|
|
610
|
+
coordinatorRepo: undefined as any
|
|
611
|
+
};
|
|
612
|
+
buildMember(meshNode, lifetime);
|
|
613
|
+
nodes.push(meshNode);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Phase 2: coordinator repos, once every member exists.
|
|
617
|
+
for (const node of nodes) {
|
|
618
|
+
buildCoordinator(node, lifetimeByPeer.get(node.peerId.toString())!);
|
|
466
619
|
}
|
|
467
620
|
|
|
468
|
-
|
|
621
|
+
const restart = (node: MeshNode): void => {
|
|
622
|
+
const peerIdStr = node.peerId.toString();
|
|
623
|
+
if (!nodes.includes(node)) {
|
|
624
|
+
throw new Error(`restart: ${peerIdStr} is not a node of this mesh`);
|
|
625
|
+
}
|
|
626
|
+
// NOTE: this severs the old instance's traffic to OTHER nodes, not its in-process path to its own
|
|
627
|
+
// member and storage repo, which still write the shared raw storage if called. After a restart
|
|
628
|
+
// between transactions only one thing could reach it: a scheduled commit retry aimed at the
|
|
629
|
+
// coordinator's OWN member, which exists only when that member threw while applying the broadcast —
|
|
630
|
+
// a local fault no spec injects today. If a spec ever does, or restarts a node MID-transaction,
|
|
631
|
+
// sever the old coordinator's local member too.
|
|
632
|
+
lifetimeByPeer.get(peerIdStr)!.stopped = true;
|
|
633
|
+
node.clusterMember.dispose();
|
|
634
|
+
const lifetime: NodeLifetime = { stopped: false };
|
|
635
|
+
lifetimeByPeer.set(peerIdStr, lifetime);
|
|
636
|
+
buildMember(node, lifetime);
|
|
637
|
+
buildCoordinator(node, lifetime);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
return { nodes, failures, keyNetwork, restart };
|
|
469
641
|
}
|
|
470
642
|
|
|
471
643
|
/**
|
|
472
644
|
* The nodes the key network keeps OUT of `blockId`'s cohort — peers that receive none of the
|
|
473
645
|
* block's cluster traffic, and so hold none of its content until something repairs them.
|
|
474
646
|
*
|
|
475
|
-
*
|
|
476
|
-
* from run to run: in a 3-node `responsibilityK: 1` mesh,
|
|
477
|
-
* peer about a third of the time, and then it receives the
|
|
478
|
-
* needs a genuinely non-responsible node has to ask the routing
|
|
647
|
+
* Unless the mesh is seeded (`keySeed`), peer ids are generated fresh per mesh, so which node is
|
|
648
|
+
* responsible for a given block is random from run to run: in a 3-node `responsibilityK: 1` mesh,
|
|
649
|
+
* `nodes[1]` is the block's sole responsible peer about a third of the time, and then it receives the
|
|
650
|
+
* writer's commit directly. A test that needs a genuinely non-responsible node has to ask the routing
|
|
651
|
+
* layer rather than assume an index.
|
|
479
652
|
*/
|
|
480
653
|
export async function nonResponsibleNodes(mesh: Mesh, blockId: string): Promise<MeshNode[]> {
|
|
481
|
-
const cohort = await mesh.keyNetwork.findCluster(
|
|
654
|
+
const cohort = await mesh.keyNetwork.findCluster(routingKeyForBlock(blockId));
|
|
482
655
|
return mesh.nodes.filter(node => !(node.peerId.toString() in cohort));
|
|
483
656
|
}
|
|
484
657
|
|
|
658
|
+
/**
|
|
659
|
+
* The nodes the key network places in `blockId`'s cohort, nearest first — the complement of
|
|
660
|
+
* {@link nonResponsibleNodes}. A node outside this list refuses a write for the block through its own
|
|
661
|
+
* coordinator (`CoordinatorRepo` checks responsibility), so a spec that writes through one node's
|
|
662
|
+
* coordinator directly picks that node from here rather than by index.
|
|
663
|
+
*/
|
|
664
|
+
export async function responsibleNodes(mesh: Mesh, blockId: string): Promise<MeshNode[]> {
|
|
665
|
+
const cohort = Object.keys(await mesh.keyNetwork.findCluster(routingKeyForBlock(blockId)));
|
|
666
|
+
return cohort.map(id => mesh.nodes.find(node => node.peerId.toString() === id)!);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* The first `count` ids of the form `${prefix}-${i}` whose cohort includes `node` — for a spec that needs
|
|
671
|
+
* several blocks one node is responsible for (a multi-block pend through that node's coordinator, or
|
|
672
|
+
* sequential writes it coordinates alone). In a `responsibilityK: 1` mesh these are blocks the node is
|
|
673
|
+
* the SOLE responsible peer for. Throws after `maxCandidates` ids rather than looping on a node whose ring
|
|
674
|
+
* arc no id lands in.
|
|
675
|
+
*/
|
|
676
|
+
export async function blockIdsInCohortOf(mesh: Mesh, node: MeshNode, count: number, prefix: string, maxCandidates = 10_000): Promise<BlockId[]> {
|
|
677
|
+
const nodeId = node.peerId.toString();
|
|
678
|
+
const ids: BlockId[] = [];
|
|
679
|
+
for (let i = 0; ids.length < count && i < maxCandidates; i++) {
|
|
680
|
+
const id = `${prefix}-${i}` as BlockId;
|
|
681
|
+
if (nodeId in await mesh.keyNetwork.findCluster(routingKeyForBlock(id))) ids.push(id);
|
|
682
|
+
}
|
|
683
|
+
if (ids.length < count) {
|
|
684
|
+
throw new Error(`blockIdsInCohortOf: only ${ids.length} of ${count} ids with prefix ${prefix} place ${nodeId} in their cohort`);
|
|
685
|
+
}
|
|
686
|
+
return ids;
|
|
687
|
+
}
|
|
688
|
+
|
|
485
689
|
export interface BuildTransactorOptions {
|
|
486
690
|
timeoutMs?: number;
|
|
487
691
|
abortOrCancelTimeoutMs?: number;
|
|
@@ -496,10 +700,30 @@ export interface BuildTransactorOptions {
|
|
|
496
700
|
/**
|
|
497
701
|
* Builds a NetworkTransactor over a mesh. All nodes share the same mock
|
|
498
702
|
* infrastructure so a single transactor routes to every peer via `getRepo`.
|
|
703
|
+
* It runs on no node (no `localPeerId`), so a coverage tie between cohort
|
|
704
|
+
* members goes to the nearest — a client-only writer's shape.
|
|
499
705
|
* Suitable for solo-mesh tests; for multi-node tests prefer
|
|
500
706
|
* `buildNetworkTransactors` to label "which node is driving".
|
|
501
707
|
*/
|
|
502
|
-
export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor =>
|
|
708
|
+
export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor =>
|
|
709
|
+
meshTransactor(mesh, options, undefined);
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Builds one NetworkTransactor per mesh node, keyed by peer-id string. Each
|
|
713
|
+
* transactor shares the mesh's key network and peer→repo map, and runs on its
|
|
714
|
+
* node the way a production node's transactor does (`localPeerId`): when that
|
|
715
|
+
* node is in a block's cohort and ties another member on coverage, it
|
|
716
|
+
* coordinates the write itself. Reads and retries still route by proximity.
|
|
717
|
+
*/
|
|
718
|
+
export const buildNetworkTransactors = (mesh: Mesh, options: BuildTransactorOptions = {}): Map<string, ITransactor> => {
|
|
719
|
+
const transactors = new Map<string, ITransactor>();
|
|
720
|
+
for (const node of mesh.nodes) {
|
|
721
|
+
transactors.set(node.peerId.toString(), meshTransactor(mesh, options, node));
|
|
722
|
+
}
|
|
723
|
+
return transactors;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
function meshTransactor(mesh: Mesh, options: BuildTransactorOptions, localNode: MeshNode | undefined): ITransactor {
|
|
503
727
|
const repoByPeer = new Map<string, IRepo>();
|
|
504
728
|
for (const node of mesh.nodes) {
|
|
505
729
|
const repo = node.coordinatorRepo as unknown as IRepo;
|
|
@@ -508,24 +732,14 @@ export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptio
|
|
|
508
732
|
return new NetworkTransactor({
|
|
509
733
|
timeoutMs: options.timeoutMs ?? 5_000,
|
|
510
734
|
abortOrCancelTimeoutMs: options.abortOrCancelTimeoutMs ?? 5_000,
|
|
735
|
+
// NOTE: every transactor, `localNode`'s included, reads the unpartitioned shared view, not `localNode.keyNetwork`;
|
|
736
|
+
// no spec drives `partitionSides` through a node's transactor today. If one does, pass the node's own view.
|
|
511
737
|
keyNetwork: mesh.keyNetwork,
|
|
512
738
|
getRepo: (peerId: DbPeerId) => {
|
|
513
739
|
const repo = repoByPeer.get(peerId.toString());
|
|
514
740
|
if (!repo) throw new Error(`Unknown peer ${peerId.toString()}`);
|
|
515
741
|
return repo;
|
|
516
|
-
}
|
|
742
|
+
},
|
|
743
|
+
localPeerId: localNode?.peerId
|
|
517
744
|
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Builds one NetworkTransactor per mesh node, keyed by peer-id string. Each
|
|
522
|
-
* transactor shares the mesh's key network and peer→repo map — the separate
|
|
523
|
-
* instances exist so tests can semantically say "driven by node A".
|
|
524
|
-
*/
|
|
525
|
-
export const buildNetworkTransactors = (mesh: Mesh, options: BuildTransactorOptions = {}): Map<string, ITransactor> => {
|
|
526
|
-
const transactors = new Map<string, ITransactor>();
|
|
527
|
-
for (const node of mesh.nodes) {
|
|
528
|
-
transactors.set(node.peerId.toString(), buildNetworkTransactor(mesh, options));
|
|
529
|
-
}
|
|
530
|
-
return transactors;
|
|
531
|
-
};
|
|
745
|
+
}
|