@optimystic/db-p2p 0.24.1 → 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 +21 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +28 -17
- 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 +78 -6
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +136 -57
- 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 +150 -62
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +2 -2
- 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 +128 -1
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +198 -15
- 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 +22 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +25 -13
- 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/routing/libp2p-known-peers.d.ts.map +1 -1
- package/dist/src/routing/libp2p-known-peers.js +5 -0
- package/dist/src/routing/libp2p-known-peers.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 +308 -293
- 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 +142 -58
- package/src/libp2p-node-base.ts +1793 -1675
- package/src/logger.ts +27 -27
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +360 -149
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +145 -144
- package/src/reactivity/push-state-gossip.ts +292 -291
- 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 +332 -313
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/routing/libp2p-known-peers.ts +31 -26
- 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/cohort-topic-mesh-harness.ts +673 -673
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,424 +1,547 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
-
import type { PartitionDetector } from './partition-detector.js';
|
|
4
|
-
import type { RestorationCoordinator } from '../storage/restoration-coordinator.js';
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
pulled
|
|
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
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const [
|
|
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
|
-
if (
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1
|
+
import type { IPeerNetwork } from '@optimystic/db-core';
|
|
2
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import type { PartitionDetector } from './partition-detector.js';
|
|
4
|
+
import type { RestorationCoordinator } from '../storage/restoration-coordinator.js';
|
|
5
|
+
import type { ProofRetainingRepo } from '../storage/block-archive.js';
|
|
6
|
+
import { BlockTransferClient, sourceBlockCertification } from './block-transfer-service.js';
|
|
7
|
+
import type { GrowthOutcome, RebalanceEvent } from './rebalance-monitor.js';
|
|
8
|
+
import { createLogger } from '../logger.js';
|
|
9
|
+
|
|
10
|
+
const log = createLogger('block-transfer');
|
|
11
|
+
|
|
12
|
+
export interface BlockTransferConfig {
|
|
13
|
+
/** Max concurrent transfers. Default: 4 */
|
|
14
|
+
maxConcurrency?: number;
|
|
15
|
+
/** Timeout per block transfer (ms). Default: 30000 */
|
|
16
|
+
transferTimeoutMs?: number;
|
|
17
|
+
/** Retry attempts for failed transfers. Default: 2 */
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
/** Whether to push blocks to new owners proactively. Default: true */
|
|
20
|
+
enablePush?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Outcome of reacting to a {@link RebalanceEvent}. The `released` list is the gate the caller opens
|
|
25
|
+
* before it may stop serving a lost block: a block appears here ONLY after it was confirmed
|
|
26
|
+
* replicated to the event's floor of new owners. Everything in `retained` stays tracked and served
|
|
27
|
+
* (its push failed, was partition-skipped, or could not reach the floor) and is retried on the next
|
|
28
|
+
* rebalance. See `docs/arachnode-ring-handoff.md` § Part 2.
|
|
29
|
+
*/
|
|
30
|
+
export interface RebalanceReactionResult {
|
|
31
|
+
/** Gained blocks successfully pulled (now durably held locally). */
|
|
32
|
+
pulled: string[];
|
|
33
|
+
/** Lost blocks confirmed replicated to ≥ floor new owners — safe to release. */
|
|
34
|
+
released: string[];
|
|
35
|
+
/** Lost blocks whose replication could not be confirmed — keep serving, retry later. */
|
|
36
|
+
retained: string[];
|
|
37
|
+
/** Grown blocks confirmed pushed to every newly co-responsible peer (capped by the floor). */
|
|
38
|
+
replicated: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Grown blocks that could not be confirmed on the new peers this pass. Nothing is released off
|
|
41
|
+
* this list — the node keeps the block either way. The retry lives in the monitor: the caller
|
|
42
|
+
* feeds each block's {@link GrowthOutcome} (in `growth`) back via
|
|
43
|
+
* `RebalanceMonitor.recordGrowthOutcome`, so an unconfirmed peer stays out of the seen set and
|
|
44
|
+
* the next check re-detects it.
|
|
45
|
+
*/
|
|
46
|
+
underReplicated: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Per-block feedback for the growth arm, keyed by block id. A block reported `grown` that the
|
|
49
|
+
* reaction had NO information about (its confirm was deduped against one already in flight) has
|
|
50
|
+
* no entry — the monitor must leave that block's state untouched.
|
|
51
|
+
*/
|
|
52
|
+
growth: Map<string, GrowthOutcome>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Coordinates block transfers in response to rebalance events.
|
|
57
|
+
*
|
|
58
|
+
* For gained blocks: delegates to RestorationCoordinator.restore() which
|
|
59
|
+
* already handles ring-based discovery and fetching.
|
|
60
|
+
*
|
|
61
|
+
* For lost blocks: proactively pushes block data to new responsible peers
|
|
62
|
+
* via the BlockTransfer protocol.
|
|
63
|
+
*/
|
|
64
|
+
export class BlockTransferCoordinator {
|
|
65
|
+
private readonly maxConcurrency: number;
|
|
66
|
+
private readonly transferTimeoutMs: number;
|
|
67
|
+
private readonly maxRetries: number;
|
|
68
|
+
private readonly enablePush: boolean;
|
|
69
|
+
private inFlight = new Set<string>();
|
|
70
|
+
private concurrency = 0;
|
|
71
|
+
private readonly waitQueue: Array<() => void> = [];
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
/**
|
|
75
|
+
* The node's OWN store, proof accessor REQUIRED (not the optional `ArchiveServingRepo`
|
|
76
|
+
* shape): every push this class makes is certified out of it, and a repo without the
|
|
77
|
+
* accessor would make `sourceBlockCertification` return meta-only for every block — so
|
|
78
|
+
* every push would be refused by a receiver running the default `requirePushCertificate`,
|
|
79
|
+
* silently and with no type error. Same reasoning as `createServedRepoProxy`'s.
|
|
80
|
+
*/
|
|
81
|
+
private readonly repo: ProofRetainingRepo,
|
|
82
|
+
private readonly peerNetwork: IPeerNetwork,
|
|
83
|
+
private readonly restorationCoordinator: RestorationCoordinator,
|
|
84
|
+
private readonly partitionDetector: PartitionDetector,
|
|
85
|
+
private readonly protocolPrefix: string = '',
|
|
86
|
+
config: BlockTransferConfig = {}
|
|
87
|
+
) {
|
|
88
|
+
this.maxConcurrency = config.maxConcurrency ?? 4;
|
|
89
|
+
this.transferTimeoutMs = config.transferTimeoutMs ?? 30000;
|
|
90
|
+
this.maxRetries = config.maxRetries ?? 2;
|
|
91
|
+
this.enablePush = config.enablePush ?? true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pull blocks that this node has gained responsibility for.
|
|
96
|
+
* Uses RestorationCoordinator to discover holders and fetch block data.
|
|
97
|
+
*/
|
|
98
|
+
async pullBlocks(blockIds: string[]): Promise<{ succeeded: string[]; failed: string[] }> {
|
|
99
|
+
if (this.partitionDetector.detectPartition()) {
|
|
100
|
+
log('pull:partition-detected, skipping %d blocks', blockIds.length);
|
|
101
|
+
return { succeeded: [], failed: blockIds };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const succeeded: string[] = [];
|
|
105
|
+
const failed: string[] = [];
|
|
106
|
+
|
|
107
|
+
const ids = blockIds.filter(id => !this.inFlight.has(`pull:${id}`));
|
|
108
|
+
|
|
109
|
+
await Promise.all(ids.map(id => this.executePull(id, succeeded, failed)));
|
|
110
|
+
|
|
111
|
+
return { succeeded, failed };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Push blocks that this node has lost responsibility for to new owners.
|
|
116
|
+
*/
|
|
117
|
+
async pushBlocks(
|
|
118
|
+
blockIds: string[],
|
|
119
|
+
newOwners: Map<string, string[]>
|
|
120
|
+
): Promise<{ succeeded: string[]; failed: string[] }> {
|
|
121
|
+
if (!this.enablePush) {
|
|
122
|
+
return { succeeded: [], failed: [] };
|
|
123
|
+
}
|
|
124
|
+
if (this.partitionDetector.detectPartition()) {
|
|
125
|
+
log('push:partition-detected, skipping %d blocks', blockIds.length);
|
|
126
|
+
return { succeeded: [], failed: blockIds };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const succeeded: string[] = [];
|
|
130
|
+
const failed: string[] = [];
|
|
131
|
+
|
|
132
|
+
const ids = blockIds.filter(id => !this.inFlight.has(`push:${id}`) && newOwners.has(id));
|
|
133
|
+
|
|
134
|
+
await Promise.all(ids.map(id => this.executePush(id, newOwners, succeeded, failed)));
|
|
135
|
+
|
|
136
|
+
return { succeeded, failed };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handle a complete rebalance event — pull gained, and **confirm** lost blocks replicated to the
|
|
141
|
+
* floor before reporting them releasable.
|
|
142
|
+
*
|
|
143
|
+
* The lost path no longer pushes fire-and-forget: it runs {@link confirmReplicated} against the
|
|
144
|
+
* event's `newOwners` and `floor`, so `released` contains only blocks that landed on ≥ floor new
|
|
145
|
+
* owners. The caller gates its `untrackBlock` (release + GC-eligibility) on `released` and leaves
|
|
146
|
+
* `retained` blocks tracked/served for the next rebalance. This closes the release-before-confirm
|
|
147
|
+
* hole (`docs/arachnode-ring-handoff.md` § Why the current code violates it #2).
|
|
148
|
+
*/
|
|
149
|
+
async handleRebalanceEvent(event: RebalanceEvent): Promise<RebalanceReactionResult> {
|
|
150
|
+
log('rebalance:start gained=%d lost=%d grown=%d floor=%d',
|
|
151
|
+
event.gained.length, event.lost.length, event.grown.size, event.floor);
|
|
152
|
+
|
|
153
|
+
const floor = Math.max(1, event.floor);
|
|
154
|
+
const [pullResult, confirmResult, growResult] = await Promise.all([
|
|
155
|
+
event.gained.length > 0 ? this.pullBlocks(event.gained) : { succeeded: [], failed: [] },
|
|
156
|
+
event.lost.length > 0 && event.newOwners.size > 0
|
|
157
|
+
? this.confirmReplicated(event.lost, event.newOwners, floor)
|
|
158
|
+
: { confirmed: [], unconfirmed: [...event.lost] },
|
|
159
|
+
this.replicateGrown(event.grown, floor)
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
log('rebalance:done pull=%d/%d released=%d/%d replicated=%d/%d',
|
|
163
|
+
pullResult.succeeded.length, event.gained.length,
|
|
164
|
+
confirmResult.confirmed.length, event.lost.length,
|
|
165
|
+
growResult.confirmed.length, event.grown.size);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
pulled: pullResult.succeeded,
|
|
169
|
+
released: confirmResult.confirmed,
|
|
170
|
+
retained: confirmResult.unconfirmed,
|
|
171
|
+
replicated: growResult.confirmed,
|
|
172
|
+
underReplicated: growResult.unconfirmed,
|
|
173
|
+
growth: growResult.growth
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Push each GROWN block (still owned; new peers became co-responsible) to its newly
|
|
179
|
+
* co-responsible peers, reusing {@link executeConfirm} per block. The per-block floor is
|
|
180
|
+
* `min(event floor, new-peer count)`: passing the raw event floor would be wrong when fewer new
|
|
181
|
+
* peers exist than the floor — `executeConfirm` could then never reach the floor and would burn
|
|
182
|
+
* `maxRetries` re-pushing peers that already accepted. Nothing is released off this path (the
|
|
183
|
+
* node KEEPS the block either way); the confirmed/unconfirmed split is reporting only. The
|
|
184
|
+
* in-flight `confirm:<id>` key dedups against a concurrent lost-confirm for the same block —
|
|
185
|
+
* such a block gets NO `growth` entry (no information), so the monitor leaves it untouched.
|
|
186
|
+
* `enablePush` deliberately does not gate this (same rationale as the NOTE in
|
|
187
|
+
* {@link confirmReplicated}); the growth arm as a whole is gated by the `rebalance.enabled`
|
|
188
|
+
* wiring in `libp2p-node-base`.
|
|
189
|
+
*
|
|
190
|
+
* Per-block {@link GrowthOutcome} rules:
|
|
191
|
+
* - floor met → all reported peers satisfied, complete. Deliberately includes peers
|
|
192
|
+
* `executeConfirm` skipped once the floor was reached — the block is adequately replicated,
|
|
193
|
+
* and re-pushing the remainder on every check forever would be a live loop.
|
|
194
|
+
* - no local data → all reported peers satisfied, complete (nothing to replicate; see NOTE).
|
|
195
|
+
* - otherwise → only the peers that actually confirmed, incomplete (retried by the monitor).
|
|
196
|
+
*/
|
|
197
|
+
private async replicateGrown(
|
|
198
|
+
grown: Map<string, string[]>,
|
|
199
|
+
floor: number
|
|
200
|
+
): Promise<{ confirmed: string[]; unconfirmed: string[]; growth: Map<string, GrowthOutcome> }> {
|
|
201
|
+
const confirmed: string[] = [];
|
|
202
|
+
const unconfirmed: string[] = [];
|
|
203
|
+
const growth = new Map<string, GrowthOutcome>();
|
|
204
|
+
if (grown.size === 0) {
|
|
205
|
+
return { confirmed, unconfirmed, growth };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this.partitionDetector.detectPartition()) {
|
|
209
|
+
// Mirrors confirmReplicated's guard (replicateGrown drives executeConfirm directly). An
|
|
210
|
+
// incomplete outcome with nothing satisfied keeps every reported peer un-seen AND counts an
|
|
211
|
+
// attempt, so a partition mid-reaction is retried like any other failed push.
|
|
212
|
+
log('grow:partition-detected, leaving %d blocks unconfirmed', grown.size);
|
|
213
|
+
for (const [blockId, newPeers] of grown) {
|
|
214
|
+
if (newPeers.length === 0) continue;
|
|
215
|
+
unconfirmed.push(blockId);
|
|
216
|
+
growth.set(blockId, { satisfiedPeers: [], complete: false });
|
|
217
|
+
}
|
|
218
|
+
return { confirmed, unconfirmed, growth };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await Promise.all([...grown.entries()].map(async ([blockId, newPeers]) => {
|
|
222
|
+
if (newPeers.length === 0) return;
|
|
223
|
+
const result = await this.executeConfirm(
|
|
224
|
+
blockId,
|
|
225
|
+
new Map([[blockId, newPeers]]),
|
|
226
|
+
Math.min(floor, newPeers.length) // both ≥ 1 here: the caller clamps floor, empty newPeers returned above
|
|
227
|
+
);
|
|
228
|
+
if (result === null) return; // confirm already in flight — no information, no entry
|
|
229
|
+
if (result.confirmed) {
|
|
230
|
+
confirmed.push(blockId);
|
|
231
|
+
growth.set(blockId, { satisfiedPeers: [...newPeers], complete: true });
|
|
232
|
+
} else if (result.noLocalData) {
|
|
233
|
+
// NOTE: nothing local to replicate (the gained∩grown first-observation case) — the
|
|
234
|
+
// reported peers are recorded satisfied so this does not become a permanent retry loop
|
|
235
|
+
// (those cohort peers are the pull's own source). If the node later obtains the block by
|
|
236
|
+
// another route (a fresh local commit, a spread push), these peers stay recorded and are
|
|
237
|
+
// never pushed — benign today, since a gained block's data comes from these very peers.
|
|
238
|
+
unconfirmed.push(blockId);
|
|
239
|
+
growth.set(blockId, { satisfiedPeers: [...newPeers], complete: true });
|
|
240
|
+
} else {
|
|
241
|
+
unconfirmed.push(blockId);
|
|
242
|
+
growth.set(blockId, { satisfiedPeers: [...result.confirmedPeers], complete: false });
|
|
243
|
+
}
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
return { confirmed, unconfirmed, growth };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Confirm each block is replicated to at least `floor` qualifying owners — the gate the ring-shift
|
|
251
|
+
* handoff (Phase B) and the rebalance release both open before a block may stop being served.
|
|
252
|
+
*
|
|
253
|
+
* For each block, this pushes to the candidate owners and counts how many report holding a current
|
|
254
|
+
* replica: a holder confirms when the push response does NOT list the block in `missing` (it either
|
|
255
|
+
* already had it or accepted the push — `handlePush` reports `accepted` only on a received-AND-persisted
|
|
256
|
+
* block). A block is `confirmed` only when that count reaches `floor`; otherwise it is `unconfirmed`
|
|
257
|
+
* and the caller keeps serving it. Per-block timeout + retry mirror {@link pushBlocks}. During a
|
|
258
|
+
* detected partition every block is left unconfirmed (consistent with the push guard), so a partition
|
|
259
|
+
* mid-handoff aborts rather than releases.
|
|
260
|
+
*
|
|
261
|
+
* @param owners blockId → candidate owner peer ids. The caller MUST have already excluded self and
|
|
262
|
+
* any same-range mover (a peer shedding the same sub-range), so every id here is a qualifying holder.
|
|
263
|
+
* @param floor required confirming owners per block (the replication floor `N`).
|
|
264
|
+
*/
|
|
265
|
+
async confirmReplicated(
|
|
266
|
+
blockIds: string[],
|
|
267
|
+
owners: Map<string, string[]>,
|
|
268
|
+
floor: number
|
|
269
|
+
): Promise<{ confirmed: string[]; unconfirmed: string[] }> {
|
|
270
|
+
// NOTE: unlike pushBlocks, this deliberately does NOT honor `enablePush` — confirmation
|
|
271
|
+
// fundamentally requires pushing to verify replication, and skipping it would leave every
|
|
272
|
+
// block unconfirmed → never released → the node never sheds. So `enablePush:false` no longer
|
|
273
|
+
// suppresses pushes on the rebalance/handoff release path (it only gates the legacy pushBlocks
|
|
274
|
+
// fire-and-forget). If a config ever needs "never move data at all", gate the release wiring,
|
|
275
|
+
// not this primitive.
|
|
276
|
+
if (this.partitionDetector.detectPartition()) {
|
|
277
|
+
log('confirm:partition-detected, leaving %d blocks unconfirmed', blockIds.length);
|
|
278
|
+
return { confirmed: [], unconfirmed: [...blockIds] };
|
|
279
|
+
}
|
|
280
|
+
if (floor <= 0) {
|
|
281
|
+
// A non-positive floor cannot be safely "met"; refuse to release rather than release for free.
|
|
282
|
+
return { confirmed: [], unconfirmed: [...blockIds] };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const confirmed: string[] = [];
|
|
286
|
+
const unconfirmed: string[] = [];
|
|
287
|
+
|
|
288
|
+
const ids = blockIds.filter(id => !this.inFlight.has(`confirm:${id}`));
|
|
289
|
+
await Promise.all(ids.map(async id => {
|
|
290
|
+
const result = await this.executeConfirm(id, owners, floor);
|
|
291
|
+
if (result === null) return; // raced into flight after the filter — no information
|
|
292
|
+
(result.confirmed ? confirmed : unconfirmed).push(id);
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
return { confirmed, unconfirmed };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async executePull(
|
|
299
|
+
blockId: string,
|
|
300
|
+
succeeded: string[],
|
|
301
|
+
failed: string[]
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
const key = `pull:${blockId}`;
|
|
304
|
+
if (this.inFlight.has(key)) return;
|
|
305
|
+
this.inFlight.add(key);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
for (let attempt = 0; ; attempt++) {
|
|
309
|
+
await this.acquireSemaphore();
|
|
310
|
+
let archive: Awaited<ReturnType<RestorationCoordinator['restore']>>;
|
|
311
|
+
try {
|
|
312
|
+
archive = await this.withTimeout(
|
|
313
|
+
this.restorationCoordinator.restore(blockId),
|
|
314
|
+
this.transferTimeoutMs
|
|
315
|
+
);
|
|
316
|
+
} finally {
|
|
317
|
+
this.releaseSemaphore();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (archive) {
|
|
321
|
+
log('pull:ok block=%s', blockId);
|
|
322
|
+
succeeded.push(blockId);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (attempt < this.maxRetries) {
|
|
326
|
+
log('pull:retry block=%s attempt=%d', blockId, attempt + 1);
|
|
327
|
+
await this.delay(this.backoffMs(attempt));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
log('pull:failed block=%s', blockId);
|
|
331
|
+
failed.push(blockId);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
this.inFlight.delete(key);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async executePush(
|
|
340
|
+
blockId: string,
|
|
341
|
+
newOwners: Map<string, string[]>,
|
|
342
|
+
succeeded: string[],
|
|
343
|
+
failed: string[]
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
const key = `push:${blockId}`;
|
|
346
|
+
if (this.inFlight.has(key)) return;
|
|
347
|
+
this.inFlight.add(key);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
for (let attempt = 0; ; attempt++) {
|
|
351
|
+
await this.acquireSemaphore();
|
|
352
|
+
let pushed = false;
|
|
353
|
+
try {
|
|
354
|
+
const owners = newOwners.get(blockId);
|
|
355
|
+
if (!owners || owners.length === 0) {
|
|
356
|
+
failed.push(blockId);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Read block data from local storage
|
|
361
|
+
const result = await this.repo.get({ blockIds: [blockId] });
|
|
362
|
+
const blockResult = result[blockId];
|
|
363
|
+
if (!blockResult?.block) {
|
|
364
|
+
log('push:no-local-data block=%s', blockId);
|
|
365
|
+
failed.push(blockId);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const blockData = new TextEncoder().encode(JSON.stringify(blockResult.block));
|
|
370
|
+
// Revision metadata AND the cohort proof for that revision, from this one unpinned
|
|
371
|
+
// read. A receiver running the default `requirePushCertificate` rejects a push with
|
|
372
|
+
// no proof, so a block whose proof this node never retained (pre-proof history, a
|
|
373
|
+
// diverged commit) simply fails to place here and is retried/kept as today.
|
|
374
|
+
const certification = await sourceBlockCertification(this.repo, blockId, blockResult);
|
|
375
|
+
|
|
376
|
+
// Push to at least one new owner
|
|
377
|
+
for (const ownerPeerIdStr of owners) {
|
|
378
|
+
try {
|
|
379
|
+
const peerId = peerIdFromString(ownerPeerIdStr);
|
|
380
|
+
const client = new BlockTransferClient(peerId, this.peerNetwork, this.protocolPrefix);
|
|
381
|
+
const response = await this.withTimeout(
|
|
382
|
+
client.pushBlocks([blockId], [blockData], 'rebalance', certification),
|
|
383
|
+
this.transferTimeoutMs
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (response && !response.missing.includes(blockId)) {
|
|
387
|
+
pushed = true;
|
|
388
|
+
log('push:ok block=%s peer=%s', blockId, ownerPeerIdStr);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
log('push:peer-error block=%s peer=%s err=%s',
|
|
393
|
+
blockId, ownerPeerIdStr, (err as Error).message);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} finally {
|
|
397
|
+
this.releaseSemaphore();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (pushed) {
|
|
401
|
+
succeeded.push(blockId);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (attempt < this.maxRetries) {
|
|
405
|
+
log('push:retry block=%s attempt=%d', blockId, attempt + 1);
|
|
406
|
+
await this.delay(this.backoffMs(attempt));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
log('push:failed block=%s', blockId);
|
|
410
|
+
failed.push(blockId);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
} finally {
|
|
414
|
+
this.inFlight.delete(key);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Confirm one block replicated to ≥ `floor` distinct qualifying owners. Reads the local block once
|
|
420
|
+
* per attempt, pushes to each candidate owner (stopping once the floor is reached), and counts
|
|
421
|
+
* distinct owners that report holding it (not `missing`). Retries the whole round up to
|
|
422
|
+
* `maxRetries` before giving up.
|
|
423
|
+
*
|
|
424
|
+
* Returns `null` when a confirm for this block is already in flight (no information — the caller
|
|
425
|
+
* must not record anything for it). Otherwise: `confirmed` iff the floor was met; `confirmedPeers`
|
|
426
|
+
* is the union of owners that confirmed across every attempt (a peer that accepted a push holds a
|
|
427
|
+
* replica even if a later round missed it — the floor decision itself stays per-round, unchanged);
|
|
428
|
+
* `noLocalData` marks the nothing-local-to-push case. The lost-block release path uses only
|
|
429
|
+
* `confirmed`; the growth arm consumes the other two.
|
|
430
|
+
*/
|
|
431
|
+
private async executeConfirm(
|
|
432
|
+
blockId: string,
|
|
433
|
+
owners: Map<string, string[]>,
|
|
434
|
+
floor: number
|
|
435
|
+
): Promise<{ confirmed: boolean; confirmedPeers: Set<string>; noLocalData: boolean } | null> {
|
|
436
|
+
const key = `confirm:${blockId}`;
|
|
437
|
+
if (this.inFlight.has(key)) return null;
|
|
438
|
+
this.inFlight.add(key);
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const allConfirmedPeers = new Set<string>();
|
|
442
|
+
const candidateOwners = owners.get(blockId) ?? [];
|
|
443
|
+
if (candidateOwners.length === 0) {
|
|
444
|
+
// No qualifying holder to confirm against — cannot release; keep serving.
|
|
445
|
+
return { confirmed: false, confirmedPeers: allConfirmedPeers, noLocalData: false };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (let attempt = 0; ; attempt++) {
|
|
449
|
+
await this.acquireSemaphore();
|
|
450
|
+
let confirmCount = 0;
|
|
451
|
+
try {
|
|
452
|
+
// Read block data from local storage once per attempt.
|
|
453
|
+
const result = await this.repo.get({ blockIds: [blockId] });
|
|
454
|
+
const blockResult = result[blockId];
|
|
455
|
+
if (!blockResult?.block) {
|
|
456
|
+
// No local bytes to prove replication with — cannot confirm; keep serving.
|
|
457
|
+
log('confirm:no-local-data block=%s', blockId);
|
|
458
|
+
return { confirmed: false, confirmedPeers: allConfirmedPeers, noLocalData: true };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const blockData = new TextEncoder().encode(JSON.stringify(blockResult.block));
|
|
462
|
+
// See executePush: meta + proof are built together from this one unpinned read, so a
|
|
463
|
+
// confirming holder either takes a certified replica or reports the block missing.
|
|
464
|
+
const certification = await sourceBlockCertification(this.repo, blockId, blockResult);
|
|
465
|
+
|
|
466
|
+
// Count DISTINCT owners that hold a current replica; stop once the floor is reached.
|
|
467
|
+
const confirmedPeers = new Set<string>();
|
|
468
|
+
for (const ownerPeerIdStr of candidateOwners) {
|
|
469
|
+
if (confirmedPeers.size >= floor) break;
|
|
470
|
+
try {
|
|
471
|
+
const peerId = peerIdFromString(ownerPeerIdStr);
|
|
472
|
+
const client = new BlockTransferClient(peerId, this.peerNetwork, this.protocolPrefix);
|
|
473
|
+
const response = await this.withTimeout(
|
|
474
|
+
client.pushBlocks([blockId], [blockData], 'rebalance', certification),
|
|
475
|
+
this.transferTimeoutMs
|
|
476
|
+
);
|
|
477
|
+
if (response && !response.missing.includes(blockId)) {
|
|
478
|
+
confirmedPeers.add(ownerPeerIdStr);
|
|
479
|
+
// NOTE: allConfirmedPeers never un-records a peer. A holder that confirms in one
|
|
480
|
+
// round and reports `missing` in a later one stays recorded, on the reasoning that
|
|
481
|
+
// `handlePush` answers non-missing only after persisting. If the receiver ever
|
|
482
|
+
// gains a path that drops a just-persisted block (an eviction sweep, a rejected
|
|
483
|
+
// revision), the growth arm would record a peer that no longer holds a replica —
|
|
484
|
+
// intersect against the LAST round's confirmedPeers instead of unioning.
|
|
485
|
+
allConfirmedPeers.add(ownerPeerIdStr);
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
log('confirm:peer-error block=%s peer=%s err=%s',
|
|
489
|
+
blockId, ownerPeerIdStr, (err as Error).message);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
confirmCount = confirmedPeers.size;
|
|
493
|
+
} finally {
|
|
494
|
+
this.releaseSemaphore();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (confirmCount >= floor) {
|
|
498
|
+
log('confirm:ok block=%s holders=%d/%d', blockId, confirmCount, floor);
|
|
499
|
+
return { confirmed: true, confirmedPeers: allConfirmedPeers, noLocalData: false };
|
|
500
|
+
}
|
|
501
|
+
if (attempt < this.maxRetries) {
|
|
502
|
+
log('confirm:retry block=%s holders=%d/%d attempt=%d', blockId, confirmCount, floor, attempt + 1);
|
|
503
|
+
await this.delay(this.backoffMs(attempt));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
log('confirm:unmet block=%s holders=%d/%d', blockId, confirmCount, floor);
|
|
507
|
+
return { confirmed: false, confirmedPeers: allConfirmedPeers, noLocalData: false };
|
|
508
|
+
}
|
|
509
|
+
} finally {
|
|
510
|
+
this.inFlight.delete(key);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- Semaphore for concurrency limiting ---
|
|
515
|
+
|
|
516
|
+
private async acquireSemaphore(): Promise<void> {
|
|
517
|
+
if (this.concurrency < this.maxConcurrency) {
|
|
518
|
+
this.concurrency++;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
await new Promise<void>(resolve => this.waitQueue.push(resolve));
|
|
522
|
+
this.concurrency++;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private releaseSemaphore(): void {
|
|
526
|
+
this.concurrency--;
|
|
527
|
+
const next = this.waitQueue.shift();
|
|
528
|
+
if (next) next();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// --- Helpers ---
|
|
532
|
+
|
|
533
|
+
private backoffMs(attempt: number): number {
|
|
534
|
+
return Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private delay(ms: number): Promise<void> {
|
|
538
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {
|
|
542
|
+
return Promise.race([
|
|
543
|
+
promise,
|
|
544
|
+
new Promise<undefined>(resolve => setTimeout(() => resolve(undefined), ms))
|
|
545
|
+
]);
|
|
546
|
+
}
|
|
547
|
+
}
|