@optimystic/db-p2p 0.24.2 → 0.25.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/{readme.md → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- 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 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- 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/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,344 +1,524 @@
|
|
|
1
|
-
import type { PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
|
-
import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId, ActionRev,
|
|
3
|
-
import type { FindCoordinatorOptions } from '@optimystic/db-core';
|
|
4
|
-
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
5
|
-
import { NetworkTransactor
|
|
6
|
-
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
7
|
-
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
8
|
-
import { ClusterMember, clusterMember, type ReconcileBlockCallback } from '../cluster/cluster-repo.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
1
|
+
import type { PeerId, PrivateKey } from '@libp2p/interface';
|
|
2
|
+
import type { IKeyNetwork, ClusterPeers, ICluster, ClusterRecord, IRepo, BlockId, ActionRev, ITransactor, ITransactionValidator, PeerId as DbPeerId } from '@optimystic/db-core';
|
|
3
|
+
import type { FindCoordinatorOptions } from '@optimystic/db-core';
|
|
4
|
+
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
5
|
+
import { NetworkTransactor } from '@optimystic/db-core';
|
|
6
|
+
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
7
|
+
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
8
|
+
import { ClusterMember, clusterMember, type ReconcileBlockCallback, type DeriveExpectedClusterCallback, type ExpectedClusterView } from '../cluster/cluster-repo.js';
|
|
9
|
+
import { createReconcileBlock } from '../cluster/reconcile-block.js';
|
|
10
|
+
import { resolveClusterPolicy, type ClusterPolicyOptions, type ResolvedClusterPolicy } from '../cluster/cluster-policy.js';
|
|
11
|
+
import { StorageRepo } from '../storage/storage-repo.js';
|
|
12
|
+
import { MemoryRawStorage } from '../storage/memory-storage.js';
|
|
13
|
+
import { BlockStorage } from '../storage/block-storage.js';
|
|
14
|
+
import type { IRawStorage } from '../storage/i-raw-storage.js';
|
|
15
|
+
import type { BlockArchive } from '../storage/struct.js';
|
|
16
|
+
import { serveBlockArchive, servableProof } from '../storage/block-archive.js';
|
|
17
|
+
import { coordinatorRepo, type ClusterLatestCallback, type CertifiedActionRev } from '../repo/coordinator-repo.js';
|
|
18
|
+
import type { CoordinatorRepo } from '../repo/coordinator-repo.js';
|
|
19
|
+
import { sortPeersByDistance, type KnownPeer } from '../routing/responsibility.js';
|
|
20
|
+
import { toString as u8ToString } from 'uint8arrays';
|
|
21
|
+
|
|
22
|
+
export interface MeshNode {
|
|
23
|
+
peerId: PeerId;
|
|
24
|
+
privateKey: PrivateKey;
|
|
25
|
+
storageRepo: StorageRepo;
|
|
26
|
+
clusterMember: ClusterMember;
|
|
27
|
+
coordinatorRepo: CoordinatorRepo;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MeshOptions {
|
|
31
|
+
responsibilityK: number;
|
|
32
|
+
/**
|
|
33
|
+
* Replication factor. OMITTED now means what it means in production: the operator declared
|
|
34
|
+
* nothing, so it resolves to `DEFAULT_CLUSTER_SIZE` (10) — NOT to `nodeCount`. An undeclared
|
|
35
|
+
* two-node mesh therefore measures its repair corroboration floor against 10 and can never
|
|
36
|
+
* repair (see `resolveClusterPolicy` in `cluster/cluster-policy.ts`); a mesh that genuinely is
|
|
37
|
+
* its node count must say so, exactly as a real deployment must.
|
|
38
|
+
*/
|
|
39
|
+
clusterSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Passed through to `resolveClusterPolicy` verbatim; this is how a mesh declares its real
|
|
42
|
+
* cohort size (`assumedClusterSize`), downsize policy, or small-cluster opt-in. When both a
|
|
43
|
+
* legacy top-level field and the matching entry here are given, the entry here wins — it is
|
|
44
|
+
* the production-shaped one.
|
|
45
|
+
*/
|
|
46
|
+
clusterPolicy?: ClusterPolicyOptions['clusterPolicy'];
|
|
47
|
+
/** Legacy shorthand for `clusterPolicy.superMajorityThreshold`. */
|
|
48
|
+
superMajorityThreshold?: number;
|
|
49
|
+
/** Legacy shorthand for `clusterPolicy.allowDownsize`. */
|
|
50
|
+
allowClusterDownsize?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Optional per-node raw-storage factory. Invoked once per node (indexed from 0)
|
|
53
|
+
* to supply the IRawStorage that backs StorageRepo. If omitted, each node gets
|
|
54
|
+
* a fresh `MemoryRawStorage`. Used by fault-injection tests to wrap the store
|
|
55
|
+
* with a crashing proxy, or by restart tests to rebuild over preserved state.
|
|
56
|
+
*/
|
|
57
|
+
rawStorageFactory?: (index: number) => IRawStorage;
|
|
58
|
+
/**
|
|
59
|
+
* Per-node member-side cluster derivation for the membership admission gate — the harness
|
|
60
|
+
* analogue of `libp2p-node-base`'s `deriveExpectedCluster` (findCluster + FRET confidence).
|
|
61
|
+
* Omitted → each member gets the production-shaped derivation over its own self-including
|
|
62
|
+
* key-network view (partition-aware, see `MeshFailureConfig.partitionSides`) with confidence
|
|
63
|
+
* from `meshConfidence` (default 1).
|
|
64
|
+
*/
|
|
65
|
+
deriveExpectedCluster?: (node: MeshNode, blockId: BlockId) => Promise<ExpectedClusterView>;
|
|
66
|
+
/**
|
|
67
|
+
* Per-node transaction validator — the harness analogue of `NodeOptions.validator`, which
|
|
68
|
+
* `libp2p-node-base` forwards straight into `clusterMember({ … validator })`. Invoked once per
|
|
69
|
+
* node (indexed from 0, in the order `Mesh.nodes` ends up in) during assembly.
|
|
70
|
+
*
|
|
71
|
+
* Omitted → no validator, and `ClusterMember.validatePendOperations` skips the whole validation
|
|
72
|
+
* step (signatures, schema hash, operations hash) — today's harness behaviour, preserved as the
|
|
73
|
+
* default so existing meshes do not suddenly re-validate transactions they were never built to
|
|
74
|
+
* satisfy. That is also production's current posture: no composition root supplies
|
|
75
|
+
* `NodeOptions.validator` yet (backlog `feat-no-deployment-validates-transactions-at-pend`).
|
|
76
|
+
*
|
|
77
|
+
* A FACTORY rather than one shared instance because enforcement is a per-node decision, and a
|
|
78
|
+
* mixed mesh — some members enforcing, some not — is exactly the case worth testing.
|
|
79
|
+
*
|
|
80
|
+
* A mesh that arms validators can also set `clusterPolicy.unvalidatablePendPolicy: 'reject'` to
|
|
81
|
+
* refuse pends that carry no `validation` payload (the single-collection `Collection.sync`
|
|
82
|
+
* shape, which a validator cannot re-check); the default 'accept' admits them unchecked.
|
|
83
|
+
*/
|
|
84
|
+
validatorFactory?: (index: number, peerId: PeerId) => ITransactionValidator;
|
|
85
|
+
/**
|
|
86
|
+
* Per-node network-size confidence (0..1) fed to the default derivation — the FRET stand-in.
|
|
87
|
+
* Default 1 (confident). Evaluated per vote, so a spec may flip it mid-test (e.g. collapse a
|
|
88
|
+
* partition side's confidence after the mesh is built). The gate's check is STRICTLY greater
|
|
89
|
+
* than its threshold (0.5), so returning the threshold itself lands on the fail-closed side.
|
|
90
|
+
*/
|
|
91
|
+
meshConfidence?: (node: MeshNode) => number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface MeshFailureConfig {
|
|
95
|
+
/** Peers that should fail on cluster update (simulate unreachable) */
|
|
96
|
+
failingPeers?: Set<string>;
|
|
97
|
+
/** Make findCluster return empty (simulate DHT failure) */
|
|
98
|
+
findClusterFails?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Peers that are unreachable on the READ path: their latest-revision consult
|
|
101
|
+
* (`ClusterLatestCallback`) REJECTS — silence the coordinator must count as "did not
|
|
102
|
+
* answer", never as the peer claiming absence — and the reconcile/acquire transfer skips
|
|
103
|
+
* them as a source. Distinct from `failingPeers`, which fails cluster (write) updates.
|
|
104
|
+
*/
|
|
105
|
+
silentPeers?: Set<string>;
|
|
106
|
+
/**
|
|
107
|
+
* Simulated network partition: each entry is one side of the split, as a set of peer-id
|
|
108
|
+
* strings. While set, a node's own key-network view (`findCluster`) answers a caller on side S
|
|
109
|
+
* with only the members of S that would otherwise be in the cohort — an UNAUTHENTICATED
|
|
110
|
+
* shrunken view, exactly what the membership admission gate exists to refuse. Callers not in
|
|
111
|
+
* any listed side see the unpartitioned cohort.
|
|
112
|
+
*
|
|
113
|
+
* This shapes cluster VIEWS (what a coordinator declares and what a member derives), not
|
|
114
|
+
* transport reachability — combine with `failingPeers`/`silentPeers` to also sever traffic.
|
|
115
|
+
* On the write path that rarely matters: a partitioned coordinator only contacts the cohort
|
|
116
|
+
* it declared, which is already its own side.
|
|
117
|
+
*/
|
|
118
|
+
partitionSides?: Set<string>[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class MockPeerNetwork implements IPeerNetwork {
|
|
122
|
+
async connect(_peerId: PeerId, _protocol: string): Promise<any> {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mock IKeyNetwork that returns peers based on XOR distance.
|
|
129
|
+
* With responsibilityK >= nodeCount, all nodes are returned.
|
|
130
|
+
* Otherwise, K-nearest by XOR distance are returned.
|
|
131
|
+
*/
|
|
132
|
+
class MockMeshKeyNetwork implements IKeyNetwork {
|
|
133
|
+
constructor(
|
|
134
|
+
private readonly nodes: MeshNode[],
|
|
135
|
+
private readonly responsibilityK: number,
|
|
136
|
+
private readonly failures: MeshFailureConfig = {}
|
|
137
|
+
) {}
|
|
138
|
+
|
|
139
|
+
async findCoordinator(key: Uint8Array, options?: Partial<FindCoordinatorOptions>): Promise<PeerId> {
|
|
140
|
+
const excluded = new Set((options?.excludedPeers ?? []).map(p => p.toString()));
|
|
141
|
+
const sorted = this.sortedByDistance(key);
|
|
142
|
+
const pick = sorted.find(n => !excluded.has(n.peerId.toString()));
|
|
143
|
+
if (!pick) {
|
|
144
|
+
throw new Error('No coordinator available for key (all candidates excluded)');
|
|
145
|
+
}
|
|
146
|
+
return pick.peerId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async findCluster(key: Uint8Array): Promise<ClusterPeers> {
|
|
150
|
+
if (this.failures.findClusterFails) {
|
|
151
|
+
return {} as ClusterPeers;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const sorted = this.sortedByDistance(key);
|
|
155
|
+
const k = Math.min(this.responsibilityK, sorted.length);
|
|
156
|
+
const selected = sorted.slice(0, k);
|
|
157
|
+
|
|
158
|
+
const peers: ClusterPeers = {};
|
|
159
|
+
for (const node of selected) {
|
|
160
|
+
peers[node.peerId.toString()] = {
|
|
161
|
+
multiaddrs: ['/ip4/127.0.0.1/tcp/8000'],
|
|
162
|
+
publicKey: u8ToString(node.peerId.publicKey!.raw, 'base64url')
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return peers;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private sortedByDistance(key: Uint8Array): MeshNode[] {
|
|
169
|
+
const knownPeers: KnownPeer[] = this.nodes.map(n => ({
|
|
170
|
+
id: n.peerId,
|
|
171
|
+
addrs: ['/ip4/127.0.0.1/tcp/8000']
|
|
172
|
+
}));
|
|
173
|
+
const sorted = sortPeersByDistance(knownPeers, key);
|
|
174
|
+
return sorted.map(kp => this.nodes.find(n => n.peerId.equals(kp.id))!);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface Mesh {
|
|
179
|
+
nodes: MeshNode[];
|
|
180
|
+
failures: MeshFailureConfig;
|
|
181
|
+
keyNetwork: IKeyNetwork;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One cohort peer's archive for a block, read straight from the sibling's `StorageRepo` — the mesh
|
|
186
|
+
* analogue of `libp2p-node-base`'s `fetchArchiveFromPeer` (a SyncClient round trip). The archive
|
|
187
|
+
* itself comes from `serveBlockArchive`, the same function `SyncService` answers a real fetch with,
|
|
188
|
+
* so the harness cannot serve a shape production would not — including the case that used to
|
|
189
|
+
* differ: a peer holding a revision whose content it cannot materialize still votes on
|
|
190
|
+
* `(rev, actionId)` here, exactly as it does over the wire.
|
|
191
|
+
*
|
|
192
|
+
* A silent peer serves no bytes either. `undefined` is the right answer: reconcile's `no-archive`
|
|
193
|
+
* outcome deliberately conflates unreachable with holds-nothing, and the production
|
|
194
|
+
* `fetchArchiveFromPeer` swallows every dial failure into the same `undefined`. (Contrast the read
|
|
195
|
+
* path's latest-revision consult, where silence REJECTS — the coordinator counts "did not answer"
|
|
196
|
+
* separately from "holds nothing".)
|
|
197
|
+
*
|
|
198
|
+
* `nodes` is captured by reference and is fully populated by the time this is invoked.
|
|
199
|
+
*
|
|
200
|
+
* NOTE: the archive served here always carries exactly ONE revision — the peer's current latest —
|
|
201
|
+
* because that is all a repo read surfaces. Enough for repair, which only ever targets one
|
|
202
|
+
* `(rev, actionId)`. If a spec ever needs a gap-fill across a revision RANGE, `serveBlockArchive`
|
|
203
|
+
* has to grow a real range and both callers get it at once.
|
|
204
|
+
*/
|
|
205
|
+
const makeFetchArchive = (nodes: MeshNode[], selfPeerId: string, failures: MeshFailureConfig) =>
|
|
206
|
+
async (peerIdStr: string, blockId: BlockId): Promise<BlockArchive | undefined> => {
|
|
207
|
+
if (peerIdStr === selfPeerId) return undefined;
|
|
208
|
+
if (failures.silentPeers?.has(peerIdStr)) return undefined;
|
|
209
|
+
const target = nodes.find(n => n.peerId.toString() === peerIdStr);
|
|
210
|
+
if (!target) return undefined;
|
|
211
|
+
return await serveBlockArchive(target.storageRepo, blockId);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Fold the mesh's operator-facing knobs into the numbers every node runs on, through the SAME
|
|
216
|
+
* resolver a real node's composition root uses (`libp2p-node-base.ts`). Named and exported rather
|
|
217
|
+
* than inlined in `createMesh` so the precedence rule below is assertable without building a mesh.
|
|
218
|
+
*
|
|
219
|
+
* Precedence: an explicit `clusterPolicy` entry wins over the matching legacy top-level field — the
|
|
220
|
+
* entry is the production-shaped one.
|
|
221
|
+
*/
|
|
222
|
+
export function resolveMeshPolicy(options: MeshOptions): ResolvedClusterPolicy {
|
|
223
|
+
return resolveClusterPolicy({
|
|
224
|
+
clusterSize: options.clusterSize,
|
|
225
|
+
clusterPolicy: {
|
|
226
|
+
...options.clusterPolicy,
|
|
227
|
+
superMajorityThreshold: options.clusterPolicy?.superMajorityThreshold ?? options.superMajorityThreshold,
|
|
228
|
+
allowDownsize: options.clusterPolicy?.allowDownsize ?? options.allowClusterDownsize
|
|
229
|
+
// `allowUnvalidatedSmallCluster` passes through UNTOUCHED and so defaults to `false`, same
|
|
230
|
+
// as `resolveClusterPolicy` — the membership admission gate is ARMED in the harness. A mesh
|
|
231
|
+
// that must transact below the safe floor says so at its own call site:
|
|
232
|
+
// createMesh(1, { responsibilityK: 1, clusterPolicy: { allowUnvalidatedSmallCluster: true } })
|
|
233
|
+
// No harness-wide re-default: a disarmed gate has to be visible where the test is read.
|
|
234
|
+
// (Solo cohorts never reach the gate anyway — CoordinatorRepo short-circuits peerCount <= 1
|
|
235
|
+
// straight to local storage — so only a genuinely undersized MULTI-peer cohort needs the
|
|
236
|
+
// opt-in.)
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Creates N interconnected mesh nodes with real components and mock transport.
|
|
243
|
+
* ClusterClient calls route directly to target ClusterMember instances.
|
|
244
|
+
*/
|
|
245
|
+
export async function createMesh(nodeCount: number, options: MeshOptions): Promise<Mesh> {
|
|
246
|
+
// NOTE: a mesh is never shut down — `Mesh` exposes no disposal seam, so each node's
|
|
247
|
+
// `ClusterMember.dispose()` is never called and its two cleanup intervals tick for the rest of
|
|
248
|
+
// the process. Harmless today: both handles are `.unref()`ed (the process still exits) and the
|
|
249
|
+
// callbacks are no-ops on an idle member, at ~40 `createMesh` sites across db-p2p's suite. If a
|
|
250
|
+
// mesh ever holds something a timer keeps alive — a real socket, a file handle, a fake clock a
|
|
251
|
+
// spec advances — give `Mesh` a `dispose()` that walks the nodes, and make the specs use it.
|
|
252
|
+
const failures: MeshFailureConfig = {};
|
|
253
|
+
|
|
254
|
+
// NOTE: one policy for the whole mesh, so every node necessarily agrees on cluster size and
|
|
255
|
+
// thresholds. That is right for repair tests, where disagreement is not the variable. A test
|
|
256
|
+
// that needs nodes to DISAGREE about the cluster (a partition where each side derives its own
|
|
257
|
+
// view — see ticket `mesh-harness-admission-gate`) has to resolve per node instead; it cannot
|
|
258
|
+
// be expressed with a single shared object.
|
|
259
|
+
//
|
|
260
|
+
// Resolved once per mesh rather than per node for a second reason: the resolver's one-line
|
|
261
|
+
// `repair-fault-tolerance` advisory then fires once instead of N times.
|
|
262
|
+
const policy = resolveMeshPolicy(options);
|
|
263
|
+
|
|
264
|
+
// Generate key pairs for all nodes
|
|
265
|
+
const keyPairs = await Promise.all(
|
|
266
|
+
Array.from({ length: nodeCount }, async () => {
|
|
267
|
+
const privateKey = await generateKeyPair('Ed25519');
|
|
268
|
+
return { peerId: peerIdFromPrivateKey(privateKey), privateKey };
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Build nodes array (partially — coordinatorRepo added after keyNetwork is ready)
|
|
273
|
+
const nodes: MeshNode[] = [];
|
|
274
|
+
const peerNetwork = new MockPeerNetwork();
|
|
275
|
+
// One real reconcile callback per node, shared between the member's commit-path `reconcileBlock`
|
|
276
|
+
// 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 in
|
|
278
|
+
// phase 1 (with the member), consumed again in phase 2 (by the coordinator), so it is stashed
|
|
279
|
+
// here keyed by peer id rather than widened onto the public `MeshNode` type.
|
|
280
|
+
const reconcileByPeer = new Map<string, ReconcileBlockCallback>();
|
|
281
|
+
|
|
282
|
+
// 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 in phase 1, and constructing it here beats
|
|
284
|
+
// a late-bound slot a closure could fire on before it is filled. Safe because `nodes` is captured
|
|
285
|
+
// by reference and only consulted at call time, after the array is fully populated.
|
|
286
|
+
const keyNetwork = new MockMeshKeyNetwork(nodes, options.responsibilityK, failures);
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* One node's own view of the key network — what `Libp2pKeyPeerNetwork` gives a real node:
|
|
290
|
+
* - `findCluster` always includes self, so a responsible member's derived view is never empty
|
|
291
|
+
* (see the empty-view guard in `cluster-repo.admitMembership`);
|
|
292
|
+
* - under a simulated partition (`failures.partitionSides`), a caller inside a side sees only
|
|
293
|
+
* 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
|
+
* The SAME instance serves both the member's admission derivation (phase 1) and the node's
|
|
296
|
+
* coordinator (phase 2), so the two sides of a node can never see different topologies.
|
|
297
|
+
*/
|
|
298
|
+
const makeNodeKeyNetwork = (selfPeerId: PeerId): IKeyNetwork => {
|
|
299
|
+
const selfStr = selfPeerId.toString();
|
|
300
|
+
return {
|
|
301
|
+
findCoordinator: (key, opts) => keyNetwork.findCoordinator(key, opts),
|
|
302
|
+
async findCluster(key) {
|
|
303
|
+
const peers = await keyNetwork.findCluster(key);
|
|
304
|
+
const side = failures.partitionSides?.find(s => s.has(selfStr));
|
|
305
|
+
if (side) {
|
|
306
|
+
for (const id of Object.keys(peers)) {
|
|
307
|
+
if (!side.has(id)) delete peers[id];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
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
|
+
return peers;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
const nodeKeyNetworkByPeer = new Map<string, IKeyNetwork>();
|
|
321
|
+
|
|
322
|
+
// Phase 1: create storage + cluster members
|
|
323
|
+
let nodeIndex = 0;
|
|
324
|
+
for (const { peerId, privateKey } of keyPairs) {
|
|
325
|
+
const index = nodeIndex++;
|
|
326
|
+
const rawStorage = options.rawStorageFactory
|
|
327
|
+
? options.rawStorageFactory(index)
|
|
328
|
+
: new MemoryRawStorage();
|
|
329
|
+
const storageRepo = new StorageRepo(
|
|
330
|
+
(blockId: BlockId) => new BlockStorage(blockId, rawStorage)
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Active reconciliation: when a member commits a block it never pended (cohort drift), or a
|
|
334
|
+
// reader holds a corroborated revision it cannot promote locally, pull the committed revision
|
|
335
|
+
// from the cohort — through the PRODUCTION quorum rules (`createReconcileBlock`): a quorum of
|
|
336
|
+
// distinct peers must agree on the target `(rev, actionId)` AND on the block content, or the
|
|
337
|
+
// pass declines, persisting nothing. `reputation` is omitted — no reputation subsystem in the
|
|
338
|
+
// harness.
|
|
339
|
+
const reconcileBlock = createReconcileBlock({
|
|
340
|
+
selfPeerId: peerId.toString(),
|
|
341
|
+
fetchArchive: makeFetchArchive(nodes, peerId.toString(), failures),
|
|
342
|
+
// Production shape: a proof reconcile verified against the agreed bytes is persisted so
|
|
343
|
+
// the repaired replica serves it onward.
|
|
344
|
+
saveReplicatedBlock: (blockId, block, source, verifiedProof) =>
|
|
345
|
+
storageRepo.saveReplicatedBlock(blockId, block, source, verifiedProof),
|
|
346
|
+
simpleMajorityThreshold: policy.simpleMajorityThreshold,
|
|
347
|
+
superMajorityThreshold: policy.superMajorityThreshold,
|
|
348
|
+
repairCorroborationClusterSize: policy.repairCorroborationClusterSize
|
|
349
|
+
});
|
|
350
|
+
reconcileByPeer.set(peerId.toString(), reconcileBlock);
|
|
351
|
+
|
|
352
|
+
const nodeKeyNetwork = makeNodeKeyNetwork(peerId);
|
|
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
|
+
};
|
|
366
|
+
|
|
367
|
+
// Member-side cluster derivation for the membership admission gate — the production shape
|
|
368
|
+
// (`libp2p-node-base.deriveExpectedCluster`): the SAME per-node key network the coordinator
|
|
369
|
+
// selects its cohort from, plus a network-size confidence. The self-including wrapper keeps a
|
|
370
|
+
// responsible member's view non-empty; `meshConfidence` is the FRET stand-in (default 1, i.e.
|
|
371
|
+
// confident — a partition spec collapses it per side).
|
|
372
|
+
const deriveExpectedCluster: DeriveExpectedClusterCallback = options.deriveExpectedCluster
|
|
373
|
+
? (blockId) => options.deriveExpectedCluster!(meshNode, blockId)
|
|
374
|
+
: async (blockId) => ({
|
|
375
|
+
peers: await nodeKeyNetwork.findCluster(new TextEncoder().encode(blockId)) ?? {},
|
|
376
|
+
confidence: options.meshConfidence?.(meshNode) ?? 1
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
meshNode.clusterMember = clusterMember({
|
|
380
|
+
storageRepo,
|
|
381
|
+
peerNetwork,
|
|
382
|
+
peerId,
|
|
383
|
+
privateKey,
|
|
384
|
+
consensusConfig: policy,
|
|
385
|
+
reconcileBlock,
|
|
386
|
+
deriveExpectedCluster,
|
|
387
|
+
// Absent by default: `undefined` here is identical to omitting the field, and
|
|
388
|
+
// `validatePendOperations` then skips the validation step entirely.
|
|
389
|
+
validator: options.validatorFactory?.(index, peerId)
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
nodes.push(meshNode);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Phase 2: coordinator repos (needs all nodes for routing; key network built in phase 1)
|
|
396
|
+
const createClusterClient = (targetPeerId: PeerId): ICluster => {
|
|
397
|
+
const target = nodes.find(n => n.peerId.equals(targetPeerId));
|
|
398
|
+
if (!target) {
|
|
399
|
+
throw new Error(`Unknown peer: ${targetPeerId.toString()}`);
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
async update(record: ClusterRecord): Promise<ClusterRecord> {
|
|
403
|
+
if (failures.failingPeers?.has(targetPeerId.toString())) {
|
|
404
|
+
throw new Error(`Peer ${targetPeerId.toString()} is unreachable`);
|
|
405
|
+
}
|
|
406
|
+
return target.clusterMember.update(record);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
// Per-node callback: reports the queried peer's latest revision, and NOTHING else. It used to
|
|
413
|
+
// also write the peer's block into local storage ("simulate data sync"), which made every
|
|
414
|
+
// read-repair assertion on this harness observe a convergence the production callback does not
|
|
415
|
+
// provide — masking exactly the defect that ticket `read-repair-cannot-transfer-block-content`
|
|
416
|
+
// existed to expose. Transfer now happens where it does in production: through
|
|
417
|
+
// `acquireBlockFromCohort` below, gated on a corroborated revision.
|
|
418
|
+
const clusterLatestCallback: ClusterLatestCallback = async (peerId: PeerId, blockId: BlockId, context?): Promise<CertifiedActionRev | undefined> => {
|
|
419
|
+
// Silence: the peer never answers. REJECTS, mirroring what a dial failure does to the
|
|
420
|
+
// production callback — the coordinator must count this as "did not answer", never as
|
|
421
|
+
// an absent claim (a resolved `undefined` remains the peer answering "I hold nothing").
|
|
422
|
+
if (failures.silentPeers?.has(peerId.toString())) {
|
|
423
|
+
throw new Error(`Peer ${peerId.toString()} is silent`);
|
|
424
|
+
}
|
|
425
|
+
const target = nodes.find(n => n.peerId.equals(peerId));
|
|
426
|
+
if (!target) return undefined;
|
|
427
|
+
const result = await target.storageRepo.get(
|
|
428
|
+
{ blockIds: [blockId], context },
|
|
429
|
+
{ skipClusterFetch: true } as any
|
|
430
|
+
);
|
|
431
|
+
const latest = result[blockId]?.state?.latest;
|
|
432
|
+
if (!latest) return undefined;
|
|
433
|
+
// The commit proof rides along exactly as it does in production, and through the SAME
|
|
434
|
+
// lookup `serveBlockArchive` uses (`servableProof`) rather than a hand-rolled one — a
|
|
435
|
+
// harness that attached proofs by its own rule would let every mesh-tier test exercise a
|
|
436
|
+
// certification path real peers do not have, or miss one they do. Production reads the
|
|
437
|
+
// proof out of the served archive; the harness reads the sibling's repo directly, so
|
|
438
|
+
// sharing the lookup is what keeps the two answers identical.
|
|
439
|
+
const proof = await servableProof(target.storageRepo, blockId, latest);
|
|
440
|
+
return proof ? { ...latest, proof } : latest;
|
|
441
|
+
};
|
|
442
|
+
// The node's own self-including (and partition-aware) key-network view, built in phase 1 —
|
|
443
|
+
// the SAME instance the member's admission derivation reads, matching real
|
|
444
|
+
// Libp2pKeyPeerNetwork behavior.
|
|
445
|
+
const nodeKeyNetwork = nodeKeyNetworkByPeer.get(node.peerId.toString())!;
|
|
446
|
+
const factory = coordinatorRepo(
|
|
447
|
+
nodeKeyNetwork,
|
|
448
|
+
createClusterClient,
|
|
449
|
+
// The SAME resolved policy the member above was built from, spread the way
|
|
450
|
+
// `libp2p-node-base` spreads it into its coordinator factory — carrying
|
|
451
|
+
// `repairCorroborationClusterSize` (the repair floor's yardstick, DEFAULT_CLUSTER_SIZE
|
|
452
|
+
// when the mesh declared nothing), the production `minAbsoluteClusterSize` (2, not the
|
|
453
|
+
// coordinator's own fallback of 3), and the `allowUnvalidatedSmallCluster` gate —
|
|
454
|
+
// ARMED (false) unless the mesh opted out at its call site.
|
|
455
|
+
{ ...policy }
|
|
456
|
+
);
|
|
457
|
+
node.coordinatorRepo = factory({
|
|
458
|
+
storageRepo: node.storageRepo,
|
|
459
|
+
localCluster: node.clusterMember,
|
|
460
|
+
localPeerId: node.peerId,
|
|
461
|
+
clusterLatestCallback,
|
|
462
|
+
// The read path's transfer mechanism — the SAME instance the member uses on the commit
|
|
463
|
+
// path, mirroring how `libp2p-node-base` shares one `reconcileBlock` between both.
|
|
464
|
+
acquireBlockFromCohort: reconcileByPeer.get(node.peerId.toString())!
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return { nodes, failures, keyNetwork };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* The nodes the key network keeps OUT of `blockId`'s cohort — peers that receive none of the
|
|
473
|
+
* block's cluster traffic, and so hold none of its content until something repairs them.
|
|
474
|
+
*
|
|
475
|
+
* Peer ids are generated fresh per mesh, so which node is responsible for a given block is random
|
|
476
|
+
* from run to run: in a 3-node `responsibilityK: 1` mesh, `nodes[1]` is the block's sole responsible
|
|
477
|
+
* peer about a third of the time, and then it receives the writer's commit directly. A test that
|
|
478
|
+
* needs a genuinely non-responsible node has to ask the routing layer rather than assume an index.
|
|
479
|
+
*/
|
|
480
|
+
export async function nonResponsibleNodes(mesh: Mesh, blockId: string): Promise<MeshNode[]> {
|
|
481
|
+
const cohort = await mesh.keyNetwork.findCluster(new TextEncoder().encode(blockId));
|
|
482
|
+
return mesh.nodes.filter(node => !(node.peerId.toString() in cohort));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export interface BuildTransactorOptions {
|
|
486
|
+
timeoutMs?: number;
|
|
487
|
+
abortOrCancelTimeoutMs?: number;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Builds a NetworkTransactor over a mesh. All nodes share the same mock
|
|
492
|
+
* infrastructure so a single transactor routes to every peer via `getRepo`.
|
|
493
|
+
* Suitable for solo-mesh tests; for multi-node tests prefer
|
|
494
|
+
* `buildNetworkTransactors` to label "which node is driving".
|
|
495
|
+
*/
|
|
496
|
+
export const buildNetworkTransactor = (mesh: Mesh, options: BuildTransactorOptions = {}): ITransactor => {
|
|
497
|
+
const repoByPeer = new Map<string, IRepo>();
|
|
498
|
+
for (const node of mesh.nodes) {
|
|
499
|
+
repoByPeer.set(node.peerId.toString(), node.coordinatorRepo as unknown as IRepo);
|
|
500
|
+
}
|
|
501
|
+
return new NetworkTransactor({
|
|
502
|
+
timeoutMs: options.timeoutMs ?? 5_000,
|
|
503
|
+
abortOrCancelTimeoutMs: options.abortOrCancelTimeoutMs ?? 5_000,
|
|
504
|
+
keyNetwork: mesh.keyNetwork,
|
|
505
|
+
getRepo: (peerId: DbPeerId) => {
|
|
506
|
+
const repo = repoByPeer.get(peerId.toString());
|
|
507
|
+
if (!repo) throw new Error(`Unknown peer ${peerId.toString()}`);
|
|
508
|
+
return repo;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Builds one NetworkTransactor per mesh node, keyed by peer-id string. Each
|
|
515
|
+
* transactor shares the mesh's key network and peer→repo map — the separate
|
|
516
|
+
* instances exist so tests can semantically say "driven by node A".
|
|
517
|
+
*/
|
|
518
|
+
export const buildNetworkTransactors = (mesh: Mesh, options: BuildTransactorOptions = {}): Map<string, ITransactor> => {
|
|
519
|
+
const transactors = new Map<string, ITransactor>();
|
|
520
|
+
for (const node of mesh.nodes) {
|
|
521
|
+
transactors.set(node.peerId.toString(), buildNetworkTransactor(mesh, options));
|
|
522
|
+
}
|
|
523
|
+
return transactors;
|
|
524
|
+
};
|