@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,312 +1,528 @@
|
|
|
1
|
-
import type { Connection, Startable, Stream } from '@libp2p/interface';
|
|
2
|
-
import type { IRepo, PeerId, IPeerNetwork, ActionId, ActionRev, IBlock, BlockId } from '@optimystic/db-core';
|
|
3
|
-
import { pipe } from 'it-pipe';
|
|
4
|
-
import * as lp from 'it-length-prefixed';
|
|
5
|
-
import { fromString as u8FromString } from 'uint8arrays/from-string';
|
|
6
|
-
import { toString as u8ToString } from 'uint8arrays/to-string';
|
|
7
|
-
import { ProtocolClient } from '../protocol-client.js';
|
|
8
|
-
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
9
|
-
import { createLogger } from '../logger.js';
|
|
10
|
-
import { createInboundStreamAuthorization, type InboundStreamAuthorization, type InboundStreamAuthorizationInit } from '../inbound-authorization.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export
|
|
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
|
-
}
|
|
1
|
+
import type { Connection, Startable, Stream } from '@libp2p/interface';
|
|
2
|
+
import type { IRepo, PeerId, IPeerNetwork, ActionId, ActionRev, GetBlockResult, IBlock, BlockId } from '@optimystic/db-core';
|
|
3
|
+
import { pipe } from 'it-pipe';
|
|
4
|
+
import * as lp from 'it-length-prefixed';
|
|
5
|
+
import { fromString as u8FromString } from 'uint8arrays/from-string';
|
|
6
|
+
import { toString as u8ToString } from 'uint8arrays/to-string';
|
|
7
|
+
import { ProtocolClient } from '../protocol-client.js';
|
|
8
|
+
import { MAX_BLOCK_MESSAGE_BYTES } from '../protocol-limits.js';
|
|
9
|
+
import { createLogger } from '../logger.js';
|
|
10
|
+
import { createInboundStreamAuthorization, type InboundStreamAuthorization, type InboundStreamAuthorizationInit } from '../inbound-authorization.js';
|
|
11
|
+
import { certifyContent, proofThresholds } from './certified-claims.js';
|
|
12
|
+
import type { BlockCommitProof, ProofThresholds } from './commit-proof.js';
|
|
13
|
+
import { servableProof, type ArchiveServingRepo } from '../storage/block-archive.js';
|
|
14
|
+
import { registerProtocolHandler } from '../network/register-protocol-handler.js';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('block-transfer-service');
|
|
17
|
+
|
|
18
|
+
/** Protocol path */
|
|
19
|
+
const BLOCK_TRANSFER_PREFIX = '/db-p2p/block-transfer/';
|
|
20
|
+
const BLOCK_TRANSFER_VERSION = '1.0.0';
|
|
21
|
+
|
|
22
|
+
export const buildBlockTransferProtocol = (protocolPrefix: string = ''): string =>
|
|
23
|
+
`${protocolPrefix}${BLOCK_TRANSFER_PREFIX}${BLOCK_TRANSFER_VERSION}`;
|
|
24
|
+
|
|
25
|
+
/** Request to transfer blocks */
|
|
26
|
+
export interface BlockTransferRequest {
|
|
27
|
+
type: 'pull' | 'push';
|
|
28
|
+
/** Block IDs being transferred */
|
|
29
|
+
blockIds: string[];
|
|
30
|
+
/** Reason for transfer */
|
|
31
|
+
reason: 'rebalance' | 'replication' | 'recovery';
|
|
32
|
+
/** For push: base64-encoded block data per block ID */
|
|
33
|
+
blockData?: Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* For push: the source's revision metadata per block ID. Carries the sender's
|
|
36
|
+
* `state.latest` so the replica's `latest` matches the source instead of being
|
|
37
|
+
* fabricated. Optional: an older sender omits it and the receiver falls back to a
|
|
38
|
+
* deterministic rev-1 replica (see {@link IBlockStorage.saveReplica}).
|
|
39
|
+
*/
|
|
40
|
+
blockMeta?: Record<string, { rev: number; actionId: ActionId }>;
|
|
41
|
+
/**
|
|
42
|
+
* For push: the cohort's commit proof for each block's revision. Verified against the pushed
|
|
43
|
+
* bytes AND against the declared {@link blockMeta} before anything is persisted — one
|
|
44
|
+
* `certifyContent` call covers the signatures and thresholds, the claim matching the declared
|
|
45
|
+
* `(rev, actionId)`, and the declared digest matching the pushed bytes.
|
|
46
|
+
*
|
|
47
|
+
* Optional on the wire so an un-upgraded sender still parses; whether a proof-less push is
|
|
48
|
+
* ACCEPTED is the receiver's `requirePushCertificate` decision (default: reject).
|
|
49
|
+
*/
|
|
50
|
+
blockProofs?: Record<string, BlockCommitProof>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The `blockMeta` a pusher sends alongside one block: the source's own `state.latest`, so the
|
|
55
|
+
* replica lands at the source's `(rev, actionId)` rather than a fabricated one. That match is what
|
|
56
|
+
* later lets the replica CORROBORATE the source in a read-repair quorum vote — a fabricated action
|
|
57
|
+
* id never matches the source's claim, so the vote sees one claimant, not two.
|
|
58
|
+
*
|
|
59
|
+
* `undefined` when the source repo reports no `latest`; the receiver then falls back to its
|
|
60
|
+
* deterministic rev-1 replica (see `IBlockStorage.saveReplica`).
|
|
61
|
+
*
|
|
62
|
+
* Every push path (rebalance confirm/push, spread-on-churn) builds it here so the wire shape stays
|
|
63
|
+
* defined in one place alongside {@link BlockTransferRequest.blockMeta}.
|
|
64
|
+
*
|
|
65
|
+
* The caller must read the block UNPINNED (no `BlockGets.context`). `state.latest` is the newest
|
|
66
|
+
* revision the source holds, while `block` is the revision `materialized` names; those agree only
|
|
67
|
+
* for an unpinned read. Pairing pinned (older) content with `latest` would label content with a
|
|
68
|
+
* revision it is not — so the pairing is guarded below and drops the meta rather than lying.
|
|
69
|
+
*
|
|
70
|
+
* Dropping, rather than labelling the older content with its own (now known) `materialized`
|
|
71
|
+
* revision, is deliberate: a push is "here is my latest replica" — the receiver lands it as its
|
|
72
|
+
* replica and the source's `latest` is what the cohort corroborates — and no push path reads
|
|
73
|
+
* pinned, so a mismatch here is a caller bug, not a historical serve. A repair fetch of an older
|
|
74
|
+
* revision is a different wire (`serveBlockArchive`), and that one DOES label from `materialized`.
|
|
75
|
+
*/
|
|
76
|
+
export function sourceBlockMeta(
|
|
77
|
+
blockId: BlockId,
|
|
78
|
+
result: Pick<GetBlockResult, 'state' | 'materialized'> | undefined
|
|
79
|
+
): Record<string, ActionRev> | undefined {
|
|
80
|
+
const latest = result?.state?.latest;
|
|
81
|
+
if (!latest) return undefined;
|
|
82
|
+
const materialized = result?.materialized;
|
|
83
|
+
if (materialized !== undefined && materialized.rev !== latest.rev) {
|
|
84
|
+
log('meta:skip block=%s materialized=%d latest=%d (pinned read — refusing to mislabel content)',
|
|
85
|
+
blockId, materialized.rev, latest.rev);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return { [blockId]: { rev: latest.rev, actionId: latest.actionId } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Everything a push carries ABOUT the blocks it pushes, as one value: the source's revision
|
|
93
|
+
* metadata and the cohort commit proof for that same revision. One parameter rather than two so a
|
|
94
|
+
* caller cannot attach a proof for one revision beside metadata for another — the two are built
|
|
95
|
+
* together by {@link sourceBlockCertification} from a single unpinned read, or not at all.
|
|
96
|
+
*/
|
|
97
|
+
export type PushCertification = {
|
|
98
|
+
blockMeta?: Record<string, ActionRev>;
|
|
99
|
+
blockProofs?: Record<string, BlockCommitProof>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a push's {@link PushCertification} for ONE block from a single unpinned read: the
|
|
104
|
+
* {@link sourceBlockMeta} for the source's `state.latest`, plus the locally-retained
|
|
105
|
+
* {@link BlockCommitProof} for exactly that revision.
|
|
106
|
+
*
|
|
107
|
+
* The proof comes from {@link servableProof} — the same accessor that decides what a peer attaches
|
|
108
|
+
* to a served repair archive — so a push and a repair fetch can never disagree about which proof
|
|
109
|
+
* pairs with which revision. It fails closed on every unhappy path (repo with no proof accessor, a
|
|
110
|
+
* throwing lookup, a stored proof whose message names a different `(blockId, rev, actionId)`), so a
|
|
111
|
+
* mis-paired artifact is never pushed.
|
|
112
|
+
*
|
|
113
|
+
* **A proof is never attached without its meta.** Without the declared `(rev, actionId)` a receiver
|
|
114
|
+
* has no claim to verify the proof against, and it rejects the block (see
|
|
115
|
+
* `BlockTransferService.handlePush`).
|
|
116
|
+
*
|
|
117
|
+
* **Meta IS still attached when no proof exists.** That is the pre-proof push, unchanged: a
|
|
118
|
+
* receiver running the default `requirePushCertificate: true` rejects it either way, and one
|
|
119
|
+
* migrating with the flag `false` needs the meta to land the replica at the source's revision
|
|
120
|
+
* instead of a fabricated rev 1. Dropping the meta alongside the missing proof would regress the
|
|
121
|
+
* legacy path without making the strict path any stricter.
|
|
122
|
+
*
|
|
123
|
+
* Like `sourceBlockMeta`, the caller must have read the block UNPINNED, so `block`, the meta and
|
|
124
|
+
* the proof all describe the same revision.
|
|
125
|
+
*/
|
|
126
|
+
export async function sourceBlockCertification(
|
|
127
|
+
repo: ArchiveServingRepo,
|
|
128
|
+
blockId: BlockId,
|
|
129
|
+
result: Pick<GetBlockResult, 'state' | 'materialized'> | undefined
|
|
130
|
+
): Promise<PushCertification> {
|
|
131
|
+
const blockMeta = sourceBlockMeta(blockId, result);
|
|
132
|
+
if (!blockMeta) {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
const latest = result!.state!.latest!;
|
|
136
|
+
const proof = await servableProof(repo, blockId, latest);
|
|
137
|
+
if (!proof) {
|
|
138
|
+
log('cert:no-local-proof block=%s rev=%d (pushing uncertified)', blockId, latest.rev);
|
|
139
|
+
return { blockMeta };
|
|
140
|
+
}
|
|
141
|
+
return { blockMeta, blockProofs: { [blockId]: proof } };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Response with block data */
|
|
145
|
+
export interface BlockTransferResponse {
|
|
146
|
+
/** Blocks successfully transferred: blockId → base64-encoded data */
|
|
147
|
+
blocks: Record<string, string>;
|
|
148
|
+
/** Block IDs that couldn't be found/transferred */
|
|
149
|
+
missing: string[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- Service (server-side handler) ---
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Repo capability the service needs: read access for `handlePull` plus a local
|
|
156
|
+
* "save replica" path for `handlePush`. The replica path must land in the node's
|
|
157
|
+
* *local* storage (not the cluster-coordinated repo), so it is a distinct method
|
|
158
|
+
* from the `IRepo` commit funnel. `StorageRepo` implements this.
|
|
159
|
+
*/
|
|
160
|
+
export interface IBlockReplicaStore extends IRepo {
|
|
161
|
+
/**
|
|
162
|
+
* Persist a replica of a block received out-of-band (churn re-replication).
|
|
163
|
+
* Seeds metadata if absent, advances `latest` monotonically, and makes the block
|
|
164
|
+
* durably servable via `get`. Idempotent for a fixed `(rev, actionId)`; a no-op
|
|
165
|
+
* (still durable) when an equal-or-newer revision is already present.
|
|
166
|
+
*
|
|
167
|
+
* `verifiedProof` MUST already have been verified against these exact bytes and this exact
|
|
168
|
+
* `source` (`certifyContent` — the digest check is what binds a proof to the content). It is
|
|
169
|
+
* retained so the receiver can re-prove onward what it just verified, instead of becoming a
|
|
170
|
+
* holder that can only corroborate.
|
|
171
|
+
*/
|
|
172
|
+
saveReplicatedBlock(blockId: BlockId, block: IBlock, source?: ActionRev, verifiedProof?: BlockCommitProof): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface BlockTransferServiceInit extends InboundStreamAuthorizationInit {
|
|
176
|
+
protocolPrefix?: string;
|
|
177
|
+
/**
|
|
178
|
+
* Reject a pushed block that carries no verifying commit proof. Default `true`.
|
|
179
|
+
* A deployment holding pre-proof data sets this `false` during migration.
|
|
180
|
+
*
|
|
181
|
+
* **Why the strict default.** `handlePush` persists what a peer hands it, and
|
|
182
|
+
* `saveReplicatedBlock` advances `latest` monotonically — so an uncertified push makes the
|
|
183
|
+
* pusher's bytes this node's authoritative revision, after which this node *corroborates* the
|
|
184
|
+
* pusher in a later read-repair vote. That is how a peer manufactures its own corroborators.
|
|
185
|
+
*
|
|
186
|
+
* The failure mode of `true` is that legacy blocks stop gaining new holders via push — visible
|
|
187
|
+
* in a `push:reject-uncertified` log line, and those blocks stay readable while two or more
|
|
188
|
+
* holders remain. The failure mode of `false` is silent acceptance of forged content. Pre-proof
|
|
189
|
+
* blocks can never be certified (the signatures no longer exist), but any block written again
|
|
190
|
+
* under the current code gets a proof, so only cold, never-updated blocks stay uncertified.
|
|
191
|
+
*
|
|
192
|
+
* With the flag `false` an uncertified push falls back to the pre-proof behaviour, logged as
|
|
193
|
+
* `push:accept-uncertified`. A push carrying a proof that FAILS verification is rejected
|
|
194
|
+
* regardless of the flag — that is not a legacy block, it is a bad one.
|
|
195
|
+
*/
|
|
196
|
+
requirePushCertificate?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface BlockTransferServiceComponents {
|
|
200
|
+
registrar: { handle: (...args: any[]) => Promise<void>; unhandle: (...args: any[]) => Promise<void> };
|
|
201
|
+
repo: IBlockReplicaStore;
|
|
202
|
+
/**
|
|
203
|
+
* The cohort's configured super-majority fraction, used to verify a pushed block's commit proof.
|
|
204
|
+
*
|
|
205
|
+
* REQUIRED rather than defaulted: the node factory must hand this down from the single resolved
|
|
206
|
+
* `consensusConfig` every other consensus consumer reads (`libp2p-node-base.ts`, which already
|
|
207
|
+
* fail-fast asserts member/coordinator coupling on exactly this value). A local default here
|
|
208
|
+
* would be a third copy that silently disagrees with a deployment that tuned the threshold.
|
|
209
|
+
*
|
|
210
|
+
* The simple-majority half of {@link ProofThresholds} is deliberately NOT threaded: it must
|
|
211
|
+
* mirror what members actually enforce (`count > total / 2`), not the configured 0.51 — see
|
|
212
|
+
* `proofThresholds` in `certified-claims.ts`, the one helper every proof-verifying path builds
|
|
213
|
+
* its thresholds with.
|
|
214
|
+
*/
|
|
215
|
+
superMajorityThreshold: number;
|
|
216
|
+
/**
|
|
217
|
+
* Optional libp2p component logger. Supplied by the node factory so authorization denials
|
|
218
|
+
* land on the same `logger.forComponent(...).error` sink as the repo/cluster/sync services;
|
|
219
|
+
* without it they fall back to this module's `debug` logger.
|
|
220
|
+
*/
|
|
221
|
+
logger?: { forComponent: (name: string) => { error: (message: string, ...args: unknown[]) => void } };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Libp2p service that handles incoming block transfer requests.
|
|
226
|
+
*
|
|
227
|
+
* Responds to pull requests by reading blocks from local storage.
|
|
228
|
+
* Handles push requests by accepting block data and storing it locally.
|
|
229
|
+
*/
|
|
230
|
+
export class BlockTransferService implements Startable {
|
|
231
|
+
private running = false;
|
|
232
|
+
private readonly protocol: string;
|
|
233
|
+
private readonly repo: IBlockReplicaStore;
|
|
234
|
+
private readonly registrar: BlockTransferServiceComponents['registrar'];
|
|
235
|
+
/** Optional embedder authorization gate; `undefined` (the default) means no check runs. */
|
|
236
|
+
private readonly authorization: InboundStreamAuthorization | undefined;
|
|
237
|
+
/** Built once from the node's configured super-majority — see {@link BlockTransferServiceComponents.superMajorityThreshold}. */
|
|
238
|
+
private readonly proofThresholds: ProofThresholds;
|
|
239
|
+
/** See {@link BlockTransferServiceInit.requirePushCertificate}. */
|
|
240
|
+
private readonly requirePushCertificate: boolean;
|
|
241
|
+
|
|
242
|
+
constructor(
|
|
243
|
+
components: BlockTransferServiceComponents,
|
|
244
|
+
init: BlockTransferServiceInit = {}
|
|
245
|
+
) {
|
|
246
|
+
this.protocol = buildBlockTransferProtocol(init.protocolPrefix ?? '');
|
|
247
|
+
this.repo = components.repo;
|
|
248
|
+
this.registrar = components.registrar;
|
|
249
|
+
this.proofThresholds = proofThresholds(components.superMajorityThreshold);
|
|
250
|
+
this.requirePushCertificate = init.requirePushCertificate ?? true;
|
|
251
|
+
const componentLog = components.logger?.forComponent('db-p2p:block-transfer');
|
|
252
|
+
this.authorization = createInboundStreamAuthorization(init, this.protocol,
|
|
253
|
+
componentLog ? (msg, ...args) => componentLog.error(msg, ...args) : (msg, ...args) => log(msg, ...args));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async start(): Promise<void> {
|
|
257
|
+
if (this.running) return;
|
|
258
|
+
await registerProtocolHandler(this.registrar, this.protocol, async (data: any, connection?: Connection) => {
|
|
259
|
+
// libp2p invokes the stream handler with the Stream as the FIRST positional argument
|
|
260
|
+
// (see cluster/repo/dispute services, which all use `(stream, connection)`). The block-
|
|
261
|
+
// transfer handler previously read `data.stream`, which is `undefined` for the positional
|
|
262
|
+
// shape — so `readRequest` ran `pipe(undefined, ...)` → "Empty pipeline", the receiver
|
|
263
|
+
// never replied, and every push/pull dialled this service hung with no response. Unwrap
|
|
264
|
+
// defensively (older shape passed `{ stream }`), mirroring sync/service.ts.
|
|
265
|
+
const stream = data?.stream ?? data;
|
|
266
|
+
await this.handleRequest(stream, connection);
|
|
267
|
+
});
|
|
268
|
+
this.running = true;
|
|
269
|
+
log('started on %s', this.protocol);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async stop(): Promise<void> {
|
|
273
|
+
if (!this.running) return;
|
|
274
|
+
await this.registrar.unhandle(this.protocol);
|
|
275
|
+
this.running = false;
|
|
276
|
+
log('stopped');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async handleRequest(stream: Stream, connection?: Connection): Promise<void> {
|
|
280
|
+
const self = this;
|
|
281
|
+
try {
|
|
282
|
+
// Authorization runs before ANY decoding or execution. Guarded on the field so a
|
|
283
|
+
// node without a predicate keeps the original path untouched.
|
|
284
|
+
if (this.authorization && await this.authorization.deny(stream, connection?.remotePeer?.toString())) return;
|
|
285
|
+
// Read the request, process it, and write the response on ONE continuous duplex
|
|
286
|
+
// pipe (mirrors cluster/repo/dispute services). The earlier read-to-end-then-write
|
|
287
|
+
// design deadlocked over a real stream: the client sends one length-prefixed request
|
|
288
|
+
// and holds its write side open awaiting the reply, so a receiver that drained the
|
|
289
|
+
// source until end-of-stream blocked forever — and the reply, written only after
|
|
290
|
+
// teardown, hit a closed stream. Yielding the response as soon as the request is read
|
|
291
|
+
// keeps both sides live.
|
|
292
|
+
const responses = pipe(
|
|
293
|
+
stream,
|
|
294
|
+
(source) => lp.decode(source, { maxDataLength: MAX_BLOCK_MESSAGE_BYTES }),
|
|
295
|
+
async function* (source) {
|
|
296
|
+
for await (const msg of source) {
|
|
297
|
+
const request = JSON.parse(u8ToString(msg.subarray(), 'utf8')) as BlockTransferRequest;
|
|
298
|
+
log('request type=%s blocks=%d reason=%s', request.type, request.blockIds.length, request.reason);
|
|
299
|
+
let response: BlockTransferResponse;
|
|
300
|
+
try {
|
|
301
|
+
response = request.type === 'pull'
|
|
302
|
+
? await self.handlePull(request)
|
|
303
|
+
: await self.handlePush(request);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
log('error: %s', (error as Error).message);
|
|
306
|
+
response = { blocks: {}, missing: [] };
|
|
307
|
+
}
|
|
308
|
+
log('response blocks=%d missing=%d', Object.keys(response.blocks).length, response.missing.length);
|
|
309
|
+
yield u8FromString(JSON.stringify(response), 'utf8');
|
|
310
|
+
return; // one request → one response per stream
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
(source) => lp.encode(source)
|
|
314
|
+
);
|
|
315
|
+
for await (const chunk of responses) {
|
|
316
|
+
stream.send(chunk);
|
|
317
|
+
}
|
|
318
|
+
await stream.close();
|
|
319
|
+
} catch (err) {
|
|
320
|
+
log('error: %s', (err as Error).message);
|
|
321
|
+
try { stream.abort(err instanceof Error ? err : new Error(String(err))); } catch { /* ignore */ }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async handlePull(request: BlockTransferRequest): Promise<BlockTransferResponse> {
|
|
326
|
+
const blocks: Record<string, string> = {};
|
|
327
|
+
const missing: string[] = [];
|
|
328
|
+
|
|
329
|
+
const result = await this.repo.get({ blockIds: request.blockIds });
|
|
330
|
+
|
|
331
|
+
for (const blockId of request.blockIds) {
|
|
332
|
+
const blockResult = result[blockId];
|
|
333
|
+
if (blockResult?.block) {
|
|
334
|
+
blocks[blockId] = Buffer.from(JSON.stringify(blockResult.block)).toString('base64');
|
|
335
|
+
} else {
|
|
336
|
+
missing.push(blockId);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { blocks, missing };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Persist pushed blocks into local storage so the new owner holds a durable
|
|
345
|
+
* replica after churn. A block is reported `accepted` only if it was both
|
|
346
|
+
* received (parseable) AND successfully persisted; a parse, certification or persist failure
|
|
347
|
+
* surfaces it as `missing` so the sender does not falsely treat it as replicated.
|
|
348
|
+
*
|
|
349
|
+
* **Certification.** A pushed block is content this node will serve — and, because
|
|
350
|
+
* `saveReplicatedBlock` advances `latest`, content it will later CORROBORATE in a read-repair
|
|
351
|
+
* vote. So by default the pusher must show the cohort's commit proof for the revision it
|
|
352
|
+
* declares, verified here against both the declared `(rev, actionId)` and the pushed bytes. See
|
|
353
|
+
* {@link BlockTransferServiceInit.requirePushCertificate} for the migration flag and its
|
|
354
|
+
* rationale.
|
|
355
|
+
*
|
|
356
|
+
* Decisions are strictly PER BLOCK: in a multi-block push a block that verifies is accepted
|
|
357
|
+
* beside one that does not. `handlePull` is unaffected — it serves this node's own storage.
|
|
358
|
+
*/
|
|
359
|
+
private async handlePush(request: BlockTransferRequest): Promise<BlockTransferResponse> {
|
|
360
|
+
const blocks: Record<string, string> = {};
|
|
361
|
+
const missing: string[] = [];
|
|
362
|
+
|
|
363
|
+
if (!request.blockData) {
|
|
364
|
+
return { blocks: {}, missing: request.blockIds };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const blockId of request.blockIds) {
|
|
368
|
+
const data = request.blockData[blockId];
|
|
369
|
+
if (!data) {
|
|
370
|
+
missing.push(blockId);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Decode + parse the wire payload into an IBlock.
|
|
375
|
+
let block: IBlock;
|
|
376
|
+
try {
|
|
377
|
+
block = JSON.parse(Buffer.from(data, 'base64').toString('utf8')) as IBlock;
|
|
378
|
+
} catch {
|
|
379
|
+
missing.push(blockId);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// `JSON.parse` accepts `null`/primitives as valid JSON. Persisting a falsy or
|
|
384
|
+
// header-less "block" would seed metadata with no materialization, making every
|
|
385
|
+
// later `get` throw. Reject such payloads as missing rather than poison storage.
|
|
386
|
+
if (block === null || typeof block !== 'object' || (block as IBlock).header === undefined) {
|
|
387
|
+
log('push:invalid block=%s (not a structurally valid block)', blockId);
|
|
388
|
+
missing.push(blockId);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Certify BEFORE persisting: `saveReplicatedBlock` advances `latest`, so anything that
|
|
393
|
+
// reaches it becomes this node's authoritative revision.
|
|
394
|
+
const source = request.blockMeta?.[blockId];
|
|
395
|
+
const proof = request.blockProofs?.[blockId];
|
|
396
|
+
let verifiedProof: BlockCommitProof | undefined;
|
|
397
|
+
if (proof !== undefined) {
|
|
398
|
+
if (!source) {
|
|
399
|
+
// No declared `(rev, actionId)` means no claim to verify the proof against, and
|
|
400
|
+
// `saveReplica` would fabricate a rev-1 replica the proof does not cover.
|
|
401
|
+
log('push:reject-uncertified block=%s reason=proof-without-meta', blockId);
|
|
402
|
+
missing.push(blockId);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// One call covers the signatures and thresholds, the claim matching the declared
|
|
406
|
+
// `(rev, actionId)`, and the declared digest matching these exact bytes. Routed through
|
|
407
|
+
// `certifyContent` rather than the raw verifier so the wire-facing signer cap
|
|
408
|
+
// (MAX_PROOF_SIGNERS) the verifier requires of its callers is applied before any
|
|
409
|
+
// signature work — this input comes from an unauthenticated peer.
|
|
410
|
+
//
|
|
411
|
+
// NOTE: no `ProofAnchoring` is threaded here, unlike the other two proof-verifying
|
|
412
|
+
// paths (`ReconcileBlockDeps.anchoring`, `CoordinatorRepo`'s). Nothing wires anchoring
|
|
413
|
+
// in production today, so all three behave identically; when
|
|
414
|
+
// `feat-cluster-membership-threshold-cert-anchoring` lands, this call needs the same
|
|
415
|
+
// optional dep or the push path becomes the one place the unanchored residual is
|
|
416
|
+
// neither compared nor surfaced.
|
|
417
|
+
//
|
|
418
|
+
// NOTE: verification cost per REQUEST is unbounded in a way per-proof caps do not
|
|
419
|
+
// cover — one inbound push frame is capped at MAX_BLOCK_MESSAGE_BYTES (8 MiB, not the
|
|
420
|
+
// 1 MiB control cap `MAX_PROOF_SIGNERS`' note reasons about) and may carry one proof
|
|
421
|
+
// per block id. Each proof is bounded (≤ 256 signers), but their number is the
|
|
422
|
+
// sender's choice. Acceptable today because the pre-proof path already did per-block
|
|
423
|
+
// parse + disk write on the same frame; if inbound push CPU ever shows up in a
|
|
424
|
+
// profile, bound the per-request proof count (or total proof bytes) here.
|
|
425
|
+
const verdict = await certifyContent(
|
|
426
|
+
proof, { blockId, rev: source.rev, actionId: source.actionId }, block, this.proofThresholds);
|
|
427
|
+
if (!verdict.contentCertified) {
|
|
428
|
+
log('push:reject-uncertified block=%s rev=%d reason=%s', blockId, source.rev, verdict.failure);
|
|
429
|
+
missing.push(blockId);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
verifiedProof = proof;
|
|
433
|
+
} else if (this.requirePushCertificate) {
|
|
434
|
+
// The documented migration case: an un-upgraded sender, or a block committed before
|
|
435
|
+
// proofs existed. Diagnosable by this line alone.
|
|
436
|
+
log('push:reject-uncertified block=%s rev=%s reason=no-proof', blockId, source?.rev);
|
|
437
|
+
missing.push(blockId);
|
|
438
|
+
continue;
|
|
439
|
+
} else {
|
|
440
|
+
log('push:accept-uncertified block=%s rev=%s (requirePushCertificate disabled)', blockId, source?.rev);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Persist locally. Only a received-AND-persisted block is reported accepted.
|
|
444
|
+
try {
|
|
445
|
+
await this.repo.saveReplicatedBlock(blockId, block, source, verifiedProof);
|
|
446
|
+
blocks[blockId] = data;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
log('persist:fail block=%s err=%s', blockId, (error as Error).message);
|
|
449
|
+
missing.push(blockId);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { blocks, missing };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Factory for creating BlockTransferService following the libp2p service pattern. */
|
|
458
|
+
export const blockTransferService = (init: BlockTransferServiceInit = {}) =>
|
|
459
|
+
(components: BlockTransferServiceComponents) => new BlockTransferService(components, init);
|
|
460
|
+
|
|
461
|
+
// --- Client ---
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Client for sending block transfer requests to remote peers.
|
|
465
|
+
*/
|
|
466
|
+
export class BlockTransferClient extends ProtocolClient {
|
|
467
|
+
private readonly protocol: string;
|
|
468
|
+
|
|
469
|
+
constructor(
|
|
470
|
+
peerId: PeerId,
|
|
471
|
+
peerNetwork: IPeerNetwork,
|
|
472
|
+
protocolPrefix: string = ''
|
|
473
|
+
) {
|
|
474
|
+
super(peerId, peerNetwork);
|
|
475
|
+
this.protocol = buildBlockTransferProtocol(protocolPrefix);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Pull blocks from the remote peer.
|
|
480
|
+
*
|
|
481
|
+
* @param options Optional per-call deadlines/cancellation forwarded to the
|
|
482
|
+
* underlying request. `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
|
|
483
|
+
* bounds waiting for the reply once connected; `signal` cancels the whole
|
|
484
|
+
* request. Omitting all of them preserves the previous uncapped behavior.
|
|
485
|
+
*/
|
|
486
|
+
async pullBlocks(
|
|
487
|
+
blockIds: string[],
|
|
488
|
+
reason: BlockTransferRequest['reason'] = 'rebalance',
|
|
489
|
+
options?: { signal?: AbortSignal; dialTimeoutMs?: number; responseTimeoutMs?: number }
|
|
490
|
+
): Promise<BlockTransferResponse> {
|
|
491
|
+
const request: BlockTransferRequest = { type: 'pull', blockIds, reason };
|
|
492
|
+
return await this.processMessage<BlockTransferResponse>(request, this.protocol, { ...options, maxDataLength: MAX_BLOCK_MESSAGE_BYTES });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Push blocks to the remote peer.
|
|
497
|
+
*
|
|
498
|
+
* @param certification What this push claims about the blocks — the source's revision metadata
|
|
499
|
+
* and the cohort commit proof for that same revision, built as one unit by
|
|
500
|
+
* {@link sourceBlockCertification}. Without a proof the receiver rejects the block under its
|
|
501
|
+
* default `requirePushCertificate: true`; without metadata it replicates at a deterministic
|
|
502
|
+
* rev-1 (and rejects any proof, which then covers nothing).
|
|
503
|
+
* @param options Optional per-call deadlines/cancellation forwarded to the
|
|
504
|
+
* underlying request. `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
|
|
505
|
+
* bounds waiting for the reply once connected (so a peer that connects but goes
|
|
506
|
+
* silent throws {@link ResponseTimeoutError} instead of hanging); `signal`
|
|
507
|
+
* cancels the whole request. Omitting all of them preserves the previous
|
|
508
|
+
* uncapped behavior.
|
|
509
|
+
*/
|
|
510
|
+
async pushBlocks(
|
|
511
|
+
blockIds: string[],
|
|
512
|
+
blockDataBuffers: Uint8Array[],
|
|
513
|
+
reason: BlockTransferRequest['reason'] = 'rebalance',
|
|
514
|
+
certification?: PushCertification,
|
|
515
|
+
options?: { signal?: AbortSignal; dialTimeoutMs?: number; responseTimeoutMs?: number }
|
|
516
|
+
): Promise<BlockTransferResponse> {
|
|
517
|
+
const blockData: Record<string, string> = {};
|
|
518
|
+
for (let i = 0; i < blockIds.length; i++) {
|
|
519
|
+
blockData[blockIds[i]!] = Buffer.from(blockDataBuffers[i]!).toString('base64');
|
|
520
|
+
}
|
|
521
|
+
const request: BlockTransferRequest = {
|
|
522
|
+
type: 'push', blockIds, reason, blockData,
|
|
523
|
+
...(certification?.blockMeta ? { blockMeta: certification.blockMeta } : {}),
|
|
524
|
+
...(certification?.blockProofs ? { blockProofs: certification.blockProofs } : {})
|
|
525
|
+
};
|
|
526
|
+
return await this.processMessage<BlockTransferResponse>(request, this.protocol, { ...options, maxDataLength: MAX_BLOCK_MESSAGE_BYTES });
|
|
527
|
+
}
|
|
528
|
+
}
|