@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,498 +1,787 @@
|
|
|
1
|
-
import type { BlockId, IBlock, Transform, ActionId, ActionRev, ActionTransform } from "@optimystic/db-core";
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
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
|
-
async
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
1
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev, ActionTransform } from "@optimystic/db-core";
|
|
2
|
+
import { applyTransform, canonicalJson, hashString } from "@optimystic/db-core";
|
|
3
|
+
import type { BlockCommitProof } from "../cluster/commit-proof.js";
|
|
4
|
+
import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
|
|
5
|
+
import type { IRawStorage } from "./i-raw-storage.js";
|
|
6
|
+
import { mergeRanges } from "./helpers.js";
|
|
7
|
+
import { RevisionNotCoveredError, type IBlockStorage } from "./i-block-storage.js";
|
|
8
|
+
import type { BlockWriteLatch } from "./block-latch.js";
|
|
9
|
+
import { createLogger } from "../logger.js";
|
|
10
|
+
|
|
11
|
+
const log = createLogger('block-storage');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default checkpoint cadence: a full materialization is retained at every `CHECKPOINT_INTERVAL`th
|
|
15
|
+
* revision (plus the tip and each range floor). This bounds the maximum replay depth for any read
|
|
16
|
+
* to at most `CHECKPOINT_INTERVAL` forward transforms. See {@link BlockStorage.pruneSupersededMaterialization}.
|
|
17
|
+
*/
|
|
18
|
+
const CHECKPOINT_INTERVAL = 32;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One revision entry of a fetched archive, after {@link BlockStorage.vetRestoredArchive} has
|
|
22
|
+
* established that its key really is a revision number and that it carries an action. The `rev` here
|
|
23
|
+
* is the KEY the entry was filed under — the number `saveRestored` writes it as — not
|
|
24
|
+
* `action.rev`, which is optional and is only cross-checked against this.
|
|
25
|
+
*/
|
|
26
|
+
type RestoredRevision = { rev: number; action: ActionTransform; block?: IBlock };
|
|
27
|
+
|
|
28
|
+
export class BlockStorage implements IBlockStorage {
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly blockId: BlockId,
|
|
31
|
+
private readonly storage: IRawStorage,
|
|
32
|
+
private readonly restoreCallback?: RestoreCallback,
|
|
33
|
+
/**
|
|
34
|
+
* Revisions where `rev % checkpointInterval === 0` retain a full materialization even after they
|
|
35
|
+
* stop being the tip. Optional (default {@link CHECKPOINT_INTERVAL}); tests inject a small value
|
|
36
|
+
* to exercise sweeping without committing 32+ revisions.
|
|
37
|
+
*/
|
|
38
|
+
private readonly checkpointInterval: number = CHECKPOINT_INTERVAL
|
|
39
|
+
) { }
|
|
40
|
+
|
|
41
|
+
async getLatest(): Promise<ActionRev | undefined> {
|
|
42
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
43
|
+
return meta?.latest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Guard every write: the token must have been minted for THIS block and must still be live. The
|
|
48
|
+
* type already proves the caller went through `acquireBlockWriteLatch`; this catches the two
|
|
49
|
+
* things the type cannot — a token for block A presented to block B's storage, and a token
|
|
50
|
+
* stashed by a callback and used after its scope released the latch.
|
|
51
|
+
*/
|
|
52
|
+
private assertLatch(latch: BlockWriteLatch): void {
|
|
53
|
+
if (latch.blockId !== this.blockId) {
|
|
54
|
+
throw new Error(`Block ${this.blockId}: write latch was acquired for block ${latch.blockId}`);
|
|
55
|
+
}
|
|
56
|
+
if (!latch.live) {
|
|
57
|
+
throw new Error(`Block ${this.blockId}: write latch has already been released`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* LOCAL-ONLY read — never fetches from a peer. A revision outside `meta.ranges` throws
|
|
63
|
+
* {@link RevisionNotCoveredError}; the one caller allowed to heal that (`StorageRepo.get`) does so
|
|
64
|
+
* with {@link restoreRevision} under the block's write latch and re-reads. Keeping the fetch out of
|
|
65
|
+
* here is what lets the commit path hold N block latches with no network I/O inside them.
|
|
66
|
+
*/
|
|
67
|
+
async getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
68
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
69
|
+
if (!meta) {
|
|
70
|
+
// No metadata at all ⇒ this node has never seen the block, and reads report it absent
|
|
71
|
+
// WITHOUT consulting `restoreCallback`. That is deliberate, not an oversight: `restoreCallback`
|
|
72
|
+
// is reachable only from restoreRevision, which a caller invokes only after THIS method has
|
|
73
|
+
// reported a coverage gap on a block that has metadata — so a never-seen block is never fetched.
|
|
74
|
+
// Attempting a fetch for it would turn every read of a genuinely non-existent block —
|
|
75
|
+
// the common case for an insert probing for a collision — into a network round trip, because
|
|
76
|
+
// storage cannot tell "nobody has this" from "I don't have this".
|
|
77
|
+
//
|
|
78
|
+
// The layer that CAN tell them apart makes that call instead: `CoordinatorRepo` acquires the
|
|
79
|
+
// block only once the cohort has corroborated a `(rev, actionId)` for it
|
|
80
|
+
// (`restoreCorroborated` → `acquireBlockFromCohort`), so an id no peer claims still costs
|
|
81
|
+
// nothing beyond the latest-query it already performed. Keep this early return as-is.
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pending-only state (metadata seeded by savePendingTransaction, nothing committed) with no
|
|
86
|
+
// revision named: "no committed base here" is an ABSENCE, not a fault — answer `undefined`.
|
|
87
|
+
// StorageRepo.get then applies any pending overlay over that absent base. A NAMED rev on a
|
|
88
|
+
// pending-only block falls through: it is covered only if an earlier restore brought it in
|
|
89
|
+
// (a restore can serve real content with `latest` still undefined), otherwise it reports the
|
|
90
|
+
// gap below and StorageRepo.get decides whether the failed restore reads as absent.
|
|
91
|
+
if (meta.latest === undefined && rev === undefined) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const targetRev = rev ?? meta.latest!.rev;
|
|
96
|
+
if (!this.inRanges(targetRev, meta.ranges)) {
|
|
97
|
+
throw new RevisionNotCoveredError(this.blockId, targetRev);
|
|
98
|
+
}
|
|
99
|
+
// A throw from here means revision records exist with no materialization anywhere under them —
|
|
100
|
+
// genuine corruption, which StorageRepo.get reports as `unmaterializable`.
|
|
101
|
+
return await this.materializeBlock(meta, targetRev);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async restoreRevision(rev: number, latch: BlockWriteLatch): Promise<void> {
|
|
105
|
+
this.assertLatch(latch);
|
|
106
|
+
// One metadata read, under the held latch: the caller's earlier `getBlock` observed a gap, but
|
|
107
|
+
// a queued-ahead restore or replica may have filled it before this latch was granted.
|
|
108
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
109
|
+
if (!meta) {
|
|
110
|
+
// Same reasoning as getBlock's early return: a never-seen block is not restored here.
|
|
111
|
+
throw new Error(`Block ${this.blockId} has no metadata; a never-seen block is not restored here.`);
|
|
112
|
+
}
|
|
113
|
+
if (this.inRanges(rev, meta.ranges)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const restored = await this.restoreBlock(rev);
|
|
118
|
+
// An archive off this wire is a peer's UNVERIFIED answer (see {@link saveRestored}), so it
|
|
119
|
+
// is vetted before a byte of it reaches storage. A rejected archive is indistinguishable to
|
|
120
|
+
// the caller from an absent one — same throw — because both mean the same thing: this node
|
|
121
|
+
// still cannot serve `rev`. The specific reason is logged rather than thrown so that
|
|
122
|
+
// `StorageRepo.get`'s healing helper keeps one rule for every restore failure.
|
|
123
|
+
const coverage = restored ? await this.vetRestoredArchive(restored, rev) : undefined;
|
|
124
|
+
if (!restored || !coverage) {
|
|
125
|
+
throw new Error(`Block ${this.blockId} revision ${rev} not found during restore attempt.`);
|
|
126
|
+
}
|
|
127
|
+
await this.saveRestored(restored);
|
|
128
|
+
|
|
129
|
+
// The vetted coverage, NOT `restored.range`. The declared range is checked for internal
|
|
130
|
+
// consistency above but is not what gets recorded — see {@link vetRestoredArchive} for why
|
|
131
|
+
// the pin has to be folded in, or the same restore repeats on every read forever.
|
|
132
|
+
meta.ranges.unshift(coverage);
|
|
133
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
134
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
138
|
+
return await this.storage.getTransaction(this.blockId, actionId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getBlockProof(rev: number): Promise<BlockCommitProof | undefined> {
|
|
142
|
+
return await this.storage.getBlockProof(this.blockId, rev);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async saveBlockProof(rev: number, proof: BlockCommitProof, latch: BlockWriteLatch): Promise<void> {
|
|
146
|
+
this.assertLatch(latch);
|
|
147
|
+
await this.storage.saveBlockProof(this.blockId, rev, proof);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getPendingTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
151
|
+
return await this.storage.getPendingTransaction(this.blockId, actionId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async *listPendingTransactions(): AsyncIterable<ActionId> {
|
|
155
|
+
yield* this.storage.listPendingTransactions(this.blockId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async savePendingTransaction(actionId: ActionId, transform: Transform, latch: BlockWriteLatch): Promise<void> {
|
|
159
|
+
this.assertLatch(latch);
|
|
160
|
+
log('pend blockId=%s actionId=%s', this.blockId, actionId);
|
|
161
|
+
let meta = await this.storage.getMetadata(this.blockId);
|
|
162
|
+
if (!meta) {
|
|
163
|
+
// A freshly-pended block holds NO committed revision, so it can reconstruct
|
|
164
|
+
// nothing yet: seed empty ranges. The first commit anchors an OPEN-ENDED span at
|
|
165
|
+
// the earliest held rev E ([E, +inf)); later commits/recover merge into it via
|
|
166
|
+
// setLatest/recover. Seeding open-ended `[[0]]` would falsely claim coverage of the
|
|
167
|
+
// un-held revs below E and disable restoreRevision's restore path.
|
|
168
|
+
//
|
|
169
|
+
// This read-then-seed is exactly the window a concurrent replica used to land in (the
|
|
170
|
+
// seed then erased its `latest`); the latch the caller holds is what closes it.
|
|
171
|
+
meta = { latest: undefined, ranges: [] };
|
|
172
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
173
|
+
}
|
|
174
|
+
await this.storage.savePendingTransaction(this.blockId, actionId, transform);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async deletePendingTransaction(actionId: ActionId, latch: BlockWriteLatch): Promise<void> {
|
|
178
|
+
this.assertLatch(latch);
|
|
179
|
+
log('cancel blockId=%s actionId=%s', this.blockId, actionId);
|
|
180
|
+
await this.storage.deletePendingTransaction(this.blockId, actionId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async *listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
184
|
+
yield* this.storage.listRevisions(this.blockId, startRev, endRev);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined, latch: BlockWriteLatch): Promise<void> {
|
|
188
|
+
this.assertLatch(latch);
|
|
189
|
+
await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async pruneSupersededMaterialization(prior: ActionRev, latch: BlockWriteLatch): Promise<void> {
|
|
193
|
+
this.assertLatch(latch);
|
|
194
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
195
|
+
// No metadata / no committed tip yet ⇒ nothing has superseded `prior`; leave it.
|
|
196
|
+
if (!meta || meta.latest === undefined) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// `prior` is the PRIOR latest, so it shares the (latest) range containing meta.latest.rev — its
|
|
200
|
+
// floor is that span's start. Retain if it is the tip, that floor, or a checkpoint rev.
|
|
201
|
+
const rangeFloor = this.rangeFloorOf(meta.latest.rev, meta.ranges);
|
|
202
|
+
if (this.isRetainedRev(prior.rev, meta.latest.rev, rangeFloor)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Redundant: `prior`'s forward transform is retained, so it stays reconstructible by replay from
|
|
206
|
+
// the nearest retained materialization below it. Delete routes to the driver's deleteMaterialized;
|
|
207
|
+
// a no-op at the driver when `prior.rev` carried no materialization (e.g. a tombstone rev).
|
|
208
|
+
await this.storage.saveMaterializedBlock(this.blockId, prior.actionId, undefined);
|
|
209
|
+
log('prune blockId=%s rev=%d actionId=%s', this.blockId, prior.rev, prior.actionId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async saveRevision(rev: number, actionId: ActionId, latch: BlockWriteLatch): Promise<void> {
|
|
213
|
+
this.assertLatch(latch);
|
|
214
|
+
await this.storage.saveRevision(this.blockId, rev, actionId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async promotePendingTransaction(actionId: ActionId, latch: BlockWriteLatch): Promise<void> {
|
|
218
|
+
this.assertLatch(latch);
|
|
219
|
+
log('commit blockId=%s actionId=%s', this.blockId, actionId);
|
|
220
|
+
await this.storage.promotePendingTransaction(this.blockId, actionId);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async setLatest(latest: ActionRev, latch: BlockWriteLatch): Promise<void> {
|
|
224
|
+
this.assertLatch(latch);
|
|
225
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
226
|
+
if (!meta) {
|
|
227
|
+
throw new Error(`Block ${this.blockId} not found`);
|
|
228
|
+
}
|
|
229
|
+
// Capture the prior latest rev BEFORE overwriting: coverage anchors to the earliest held rev.
|
|
230
|
+
const prevRev = meta.latest?.rev;
|
|
231
|
+
meta.latest = latest;
|
|
232
|
+
// NOTE: re-sorts (mergeRanges) the whole ranges array on every commit; if a block ever
|
|
233
|
+
// accumulates many disjoint ranges and commits show as slow, keep a running merged structure.
|
|
234
|
+
// `getBlock(r)` is served by materializeBlock's DESCENDING walk (highest committed rev <= r).
|
|
235
|
+
// Once this node holds the chain from the block's earliest committed rev E, EVERY rev >= E is
|
|
236
|
+
// serveable locally: a read at any r >= E resolves to the highest committed rev <= r (at worst
|
|
237
|
+
// the latest, which is materialized), so coverage is the OPEN-ENDED span [E, +inf) — not the
|
|
238
|
+
// single point [L, L+1) (which wrongly missed reads above L, e.g. a block read at the collection
|
|
239
|
+
// tip after a later commit touched only its siblings) and not [0, +inf) (which wrongly claimed
|
|
240
|
+
// the un-held revs below E). Claim open-ended from the prior latest (>= E via merge); the first
|
|
241
|
+
// commit (prevRev undefined) anchors the span at E = L. mergeRanges folds it into the existing
|
|
242
|
+
// [E, +inf). Only revs BELOW E miss inRanges, which is exactly the genuine-gap/restore case.
|
|
243
|
+
// Range + latest advance in one saveMetadata write (atomic under the block write latch), so a crash
|
|
244
|
+
// before this call advances neither.
|
|
245
|
+
meta.ranges.unshift([prevRev ?? latest.rev]);
|
|
246
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
247
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async recover(latch: BlockWriteLatch): Promise<{ reconciled: boolean; latest?: ActionRev }> {
|
|
251
|
+
this.assertLatch(latch);
|
|
252
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
253
|
+
if (!meta) {
|
|
254
|
+
return { reconciled: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const currentRev = meta.latest?.rev ?? 0;
|
|
258
|
+
let maxRev = currentRev;
|
|
259
|
+
let maxActionId = meta.latest?.actionId;
|
|
260
|
+
|
|
261
|
+
// Probe forward until we hit a gap or a revision whose action is not yet
|
|
262
|
+
// in the committed log (Crash-D2 state — retry-commit owns that advance).
|
|
263
|
+
for (let next = currentRev + 1; ; next++) {
|
|
264
|
+
const actionId = await this.storage.getRevision(this.blockId, next);
|
|
265
|
+
if (actionId === undefined) break;
|
|
266
|
+
const promoted = await this.storage.getTransaction(this.blockId, actionId);
|
|
267
|
+
if (promoted === undefined) break;
|
|
268
|
+
maxRev = next;
|
|
269
|
+
maxActionId = actionId;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (maxRev > currentRev && maxActionId !== undefined) {
|
|
273
|
+
const advanced: ActionRev = { rev: maxRev, actionId: maxActionId };
|
|
274
|
+
meta.latest = advanced;
|
|
275
|
+
// The lost setLatest would have merged each recovered revision's range; redo that
|
|
276
|
+
// here. Open-ended from currentRev+1 (see setLatest): every rev in (currentRev, maxRev]
|
|
277
|
+
// was verified present in the committed log above, and any rev > maxRev resolves via the
|
|
278
|
+
// descending walk to maxRev's materialization — so [currentRev+1, +inf) is honest. It joins
|
|
279
|
+
// the prior [E, currentRev+1) (from the earlier setLatest) into one open-ended [E, +inf).
|
|
280
|
+
meta.ranges.unshift([currentRev + 1]);
|
|
281
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
282
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
283
|
+
log('recover blockId=%s advanced latest from rev=%d to rev=%d', this.blockId, currentRev, maxRev);
|
|
284
|
+
return { reconciled: true, latest: advanced };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { reconciled: false, latest: meta.latest };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async saveReplica(block: IBlock, source: ActionRev | undefined, proof: BlockCommitProof | undefined, latch: BlockWriteLatch): Promise<ActionRev> {
|
|
291
|
+
this.assertLatch(latch);
|
|
292
|
+
const rev = source?.rev ?? 1;
|
|
293
|
+
// Deterministic fallback id when the sender carried no revision metadata, so a
|
|
294
|
+
// re-push of the same block resolves to the same (rev, actionId) and stays
|
|
295
|
+
// idempotent. Never random/time-based — that would mint a new revision per retry.
|
|
296
|
+
const actionId = source?.actionId ?? await hashString(`${this.blockId}:${JSON.stringify(block)}`);
|
|
297
|
+
|
|
298
|
+
// Replica revision carries the materialized block. `{ insert: block }` satisfies saveRestored's
|
|
299
|
+
// write invariants; on the serving path materializeBlock returns the materialized block directly
|
|
300
|
+
// (single rev), so this transform is never applied — see ticket notes.
|
|
301
|
+
return await this.saveForwardRevision(
|
|
302
|
+
rev,
|
|
303
|
+
actionId,
|
|
304
|
+
{ action: { actionId, rev, transform: { insert: block } }, block },
|
|
305
|
+
'replica',
|
|
306
|
+
proof
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async saveDeletion(source: ActionRev, latch: BlockWriteLatch): Promise<ActionRev> {
|
|
311
|
+
this.assertLatch(latch);
|
|
312
|
+
const { rev, actionId } = source;
|
|
313
|
+
|
|
314
|
+
// Forward tombstone: a `{ delete: true }` transform and NO materialized block. saveRestored
|
|
315
|
+
// skips materialization when `block` is absent, so the reverse-apply in materializeBlock
|
|
316
|
+
// resolves this revision to an absent block (read-back as undefined).
|
|
317
|
+
return await this.saveForwardRevision(
|
|
318
|
+
rev,
|
|
319
|
+
actionId,
|
|
320
|
+
{ action: { actionId, rev, transform: { delete: true } } },
|
|
321
|
+
'deletion'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Shared forward-write path for saveReplica and saveDeletion. Both append a single new revision
|
|
327
|
+
* that ADVANCES `latest` (never rewrites history): acquire the block's metadata latch, apply the
|
|
328
|
+
* monotonic guard, saveRestored a one-revision archive, then seed/advance/merge metadata.
|
|
329
|
+
*
|
|
330
|
+
* The only per-caller difference is the revision `body`: a replica carries `{ insert: block }`
|
|
331
|
+
* plus the materialized `block`; a deletion carries `{ delete: true }` and no block. `rev` and
|
|
332
|
+
* `actionId` are passed alongside `body` because the guard and the `latest` advance need them
|
|
333
|
+
* independently of the archive body.
|
|
334
|
+
*
|
|
335
|
+
* `verifiedProof` travels OUTSIDE `body` on purpose — see {@link saveRestored}: it is the one
|
|
336
|
+
* channel that persists a proof, and only {@link saveReplica} (whose caller verified the proof
|
|
337
|
+
* against `body.block`) supplies it.
|
|
338
|
+
*/
|
|
339
|
+
private async saveForwardRevision(
|
|
340
|
+
rev: number,
|
|
341
|
+
actionId: ActionId,
|
|
342
|
+
body: { action: ActionTransform; block?: IBlock },
|
|
343
|
+
logLabel: 'replica' | 'deletion',
|
|
344
|
+
verifiedProof?: BlockCommitProof
|
|
345
|
+
): Promise<ActionRev> {
|
|
346
|
+
// The read-modify-write of this block's metadata below is serialized by the write latch the
|
|
347
|
+
// caller already holds (asserted in saveReplica / saveDeletion) — the same latch every other
|
|
348
|
+
// writer of this block holds, so a concurrent replica, deletion, restore, commit, or pend
|
|
349
|
+
// cannot land inside the window between the read and the saveMetadata.
|
|
350
|
+
let meta = await this.storage.getMetadata(this.blockId);
|
|
351
|
+
|
|
352
|
+
// Monotonic guard: an equal-or-newer revision is already held. The block (or tombstone) is
|
|
353
|
+
// durably present; do not downgrade `latest` or rewrite the metadata.
|
|
354
|
+
//
|
|
355
|
+
// This skip returns before persisting anything, INCLUDING `verifiedProof` — deliberately.
|
|
356
|
+
// The proof was verified against the PUSHED bytes; persisting it here would attach it to
|
|
357
|
+
// this node's HELD materialization, whose bytes at the same `(rev, actionId)` may differ if
|
|
358
|
+
// this holder diverged. A stored proof whose declared digest contradicts local content makes
|
|
359
|
+
// this node serve content that fails its own proof, and `digest-mismatch` is ATTRIBUTABLE in
|
|
360
|
+
// `cluster/certified-claims.ts` — every receiver would penalize it.
|
|
361
|
+
//
|
|
362
|
+
// Back-filling a proof onto an already-held revision therefore happens one layer up, in
|
|
363
|
+
// `StorageRepo.saveReplicatedBlock`'s non-advancing branch, which routes it through
|
|
364
|
+
// `backFillProof` → `persistProofIfContentMatches` — the rule that persists only when the
|
|
365
|
+
// LOCAL materialization matches the digest the commit op declared. Keep this guard a true
|
|
366
|
+
// no-op; the digest check is what makes the back-fill safe, and it does not belong here.
|
|
367
|
+
if (meta?.latest && meta.latest.rev >= rev) {
|
|
368
|
+
log('%s:skip blockId=%s rev=%d held=%d', logLabel, this.blockId, rev, meta.latest.rev);
|
|
369
|
+
return meta.latest;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// One-revision archive. A replica's body carries the materialized block; a deletion's body
|
|
373
|
+
// omits it (forward tombstone). saveRestored skips materialization when `block` is absent,
|
|
374
|
+
// so a tombstone reverse-applies to an absent block (read back as undefined).
|
|
375
|
+
const archive: BlockArchive = {
|
|
376
|
+
blockId: this.blockId,
|
|
377
|
+
revisions: {
|
|
378
|
+
[rev]: body
|
|
379
|
+
},
|
|
380
|
+
range: [rev, rev + 1]
|
|
381
|
+
};
|
|
382
|
+
await this.saveRestored(archive, verifiedProof ? { rev, proof: verifiedProof } : undefined);
|
|
383
|
+
|
|
384
|
+
// INVARIANT P: a block never holds a pending record AND a committed record for the same
|
|
385
|
+
// action id. On the commit path `promotePendingTransaction` maintains it by MOVING the
|
|
386
|
+
// record atomically; this forward path writes the committed transform directly (via
|
|
387
|
+
// saveRestored above), so it owes the deletion itself. Without it, a node that pended the
|
|
388
|
+
// action but diverged before committing keeps a record nothing can ever promote — reported
|
|
389
|
+
// as a phantom conflicting action by every later `pend` on the block, which under
|
|
390
|
+
// `policy: 'f'` refuses that node's participation in the block's writes permanently.
|
|
391
|
+
//
|
|
392
|
+
// Deliberately on the WRITE path only: the monotonic guard above returns before here, and
|
|
393
|
+
// that early return must stay a true no-op (the earlier call that wrote the revision is the
|
|
394
|
+
// one that owed the deletion). Deliberately here rather than in `saveRestored`, which is
|
|
395
|
+
// also reached from restoreRevision's historical restore, where a held revision's pending
|
|
396
|
+
// record is not this writer's to delete. Both paths run under the block's write latch, so
|
|
397
|
+
// this deletion is already mutually exclusive with a live commit.
|
|
398
|
+
//
|
|
399
|
+
// NOTE: deletes only this revision's actionId, not every pending whose action is already
|
|
400
|
+
// committed. A broader sweep would repair records orphaned by routes that do not carry the
|
|
401
|
+
// committing actionId; if orphaned pendings ever show up in the field on blocks whose
|
|
402
|
+
// committing action id differs, widen to a sweep over listPendingTransactions filtered by
|
|
403
|
+
// getTransaction.
|
|
404
|
+
await this.storage.deletePendingTransaction(this.blockId, actionId);
|
|
405
|
+
|
|
406
|
+
// Seed metadata when absent, advance latest, and merge the covered range.
|
|
407
|
+
const prevRev = meta?.latest?.rev;
|
|
408
|
+
if (!meta) {
|
|
409
|
+
meta = { latest: undefined, ranges: [] };
|
|
410
|
+
}
|
|
411
|
+
meta.latest = { rev, actionId };
|
|
412
|
+
// Open-ended coverage from the earliest held rev (see setLatest): the descending walk serves
|
|
413
|
+
// any rev >= the anchor. A prior latest at prevRev (< rev per the monotonic guard) is a
|
|
414
|
+
// materialized point, so anchor at prevRev; the first write (prevRev undefined) anchors at
|
|
415
|
+
// rev. Freshness of a stale replica is a separate (replication-lag) concern from what this
|
|
416
|
+
// node can locally reconstruct, which is exactly what ranges records.
|
|
417
|
+
meta.ranges.unshift([prevRev ?? rev]);
|
|
418
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
419
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
420
|
+
|
|
421
|
+
log('%s:save blockId=%s rev=%d actionId=%s', logLabel, this.blockId, rev, actionId);
|
|
422
|
+
return meta.latest;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private async materializeBlock(meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
426
|
+
let block: IBlock | undefined;
|
|
427
|
+
let materializedActionRev: ActionRev | undefined;
|
|
428
|
+
const actions: ActionRev[] = [];
|
|
429
|
+
|
|
430
|
+
// Find the materialized block
|
|
431
|
+
for await (const actionRev of this.storage.listRevisions(this.blockId, targetRev, 1)) {
|
|
432
|
+
const materializedBlock = await this.storage.getMaterializedBlock(this.blockId, actionRev.actionId);
|
|
433
|
+
if (materializedBlock) {
|
|
434
|
+
block = materializedBlock;
|
|
435
|
+
materializedActionRev = actionRev;
|
|
436
|
+
break;
|
|
437
|
+
} else {
|
|
438
|
+
actions.push(actionRev);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!block || !materializedActionRev) {
|
|
443
|
+
// There is an implicit requirement that there must be a materialization of the block somewhere in it's history. If the log is truncated, a materialization must be made at the truncation point..
|
|
444
|
+
throw new Error(`Failed to find materialized block ${this.blockId} for revision ${targetRev}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Apply transforms in reverse order
|
|
448
|
+
for (let i = actions.length - 1; i >= 0; --i) {
|
|
449
|
+
const { actionId } = actions[i]!;
|
|
450
|
+
const transform = await this.storage.getTransaction(this.blockId, actionId);
|
|
451
|
+
if (!transform) {
|
|
452
|
+
throw new Error(`Missing action ${actionId} for block ${this.blockId}`);
|
|
453
|
+
}
|
|
454
|
+
block = applyTransform(block, transform);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!block) {
|
|
458
|
+
// The reverse-apply collapsed to a tombstone (a `{ delete: true }` revision) — the block
|
|
459
|
+
// is absent at this revision, not corrupt. Read it back as undefined (matching getBlock's
|
|
460
|
+
// "no materialized content" contract) rather than throwing. The genuine-truncation throw
|
|
461
|
+
// ("Failed to find materialized block") above still fires when no materialization exists.
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
if (actions.length) {
|
|
465
|
+
// Re-cache the recomputed materialization ONLY at a retained rev (checkpoint / range floor /
|
|
466
|
+
// tip). Caching unconditionally would let a cold read at a non-checkpoint historical rev
|
|
467
|
+
// re-add a materialization the checkpoint sweep is designed to remove — storage would regrow
|
|
468
|
+
// via reads. Skipping it means a repeated cold read re-replays each time, bounded by
|
|
469
|
+
// `checkpointInterval` transforms.
|
|
470
|
+
// NOTE: cold non-checkpoint historical reads re-replay every time (up to `checkpointInterval`
|
|
471
|
+
// forward transforms). Acceptable — historical reads are rare and replay is depth-bounded. If
|
|
472
|
+
// they ever show as hot, cache at the nearest checkpoint below the target instead of skipping.
|
|
473
|
+
// NOTE: this `saveMaterializedBlock` is the ONE named exclusion from the storage invariant that
|
|
474
|
+
// every write to a block holds `blockWriteLatchKey(blockId)` (see block-latch.ts). It runs on the
|
|
475
|
+
// READ path, unlatched, and that is safe for CONTENT because it is not a read-modify-write of
|
|
476
|
+
// anything: the key is `(blockId, actionId)` and the value is a deterministic replay of
|
|
477
|
+
// transforms this node has already retained, so a concurrent SAVE of the same key writes the
|
|
478
|
+
// same bytes. It touches neither the metadata blob nor any revision record, so it cannot
|
|
479
|
+
// clobber `latest`. Taking the latch here would put a lock acquisition on every cold historical
|
|
480
|
+
// read and would deadlock the callers that already hold it. Dropping the re-cache entirely is
|
|
481
|
+
// the other way to close the exclusion, and is out of scope until someone measures the cold
|
|
482
|
+
// historical-read cost of doing without it.
|
|
483
|
+
//
|
|
484
|
+
// NOTE: the one racer that does NOT write the same bytes at this key is
|
|
485
|
+
// `pruneSupersededMaterialization`, which DELETES it (saveMaterializedBlock(..., undefined)).
|
|
486
|
+
// Losing that race resurrects a materialization the sweep just removed — a bounded storage
|
|
487
|
+
// leak, never wrong content, since the resurrected bytes are a correct materialization of that
|
|
488
|
+
// rev. The fresh `retentionMeta` read below narrows the window but cannot close it: a commit
|
|
489
|
+
// can land and prune between that read and this save. If materialization storage is ever seen
|
|
490
|
+
// to grow under read load, close it by having the prune win — e.g. re-check retention inside
|
|
491
|
+
// the raw driver's save, or have the sweep re-run after the read.
|
|
492
|
+
//
|
|
493
|
+
// Read metadata FRESH for the retention decision: because this runs outside the block's write
|
|
494
|
+
// latch, a concurrent restore or commit may have moved `ranges` since `meta` was captured. A stale
|
|
495
|
+
// `meta.ranges` would send rangeFloorOf into its fallback (treats the target as its own floor
|
|
496
|
+
// ⇒ wrongly "retained"), re-caching a rev the sweep means to prune — regrowing storage via reads.
|
|
497
|
+
const retentionMeta = (await this.storage.getMetadata(this.blockId)) ?? meta;
|
|
498
|
+
const cacheRev = actions[0]!.rev;
|
|
499
|
+
const latestRev = retentionMeta.latest?.rev ?? cacheRev;
|
|
500
|
+
const rangeFloor = this.rangeFloorOf(cacheRev, retentionMeta.ranges);
|
|
501
|
+
if (this.isRetainedRev(cacheRev, latestRev, rangeFloor)) {
|
|
502
|
+
await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
|
|
503
|
+
}
|
|
504
|
+
return { block, actionRev: actions[0]! };
|
|
505
|
+
}
|
|
506
|
+
return { block, actionRev: materializedActionRev };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async restoreBlock(rev: number): Promise<BlockArchive | undefined> {
|
|
510
|
+
if (!this.restoreCallback) return undefined;
|
|
511
|
+
return await this.restoreCallback(this.blockId, rev);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Vet an archive fetched for a PINNED restore of `rev`, returning the revision coverage to record
|
|
516
|
+
* for it — or `undefined` when the archive must be refused, in which case nothing is written at
|
|
517
|
+
* all and the reason is logged.
|
|
518
|
+
*
|
|
519
|
+
* This is the whole trust boundary for the restore wire. `restoreBlock`'s
|
|
520
|
+
* `RestorationCoordinator` verifies nothing about a response (`queryPeer` returns
|
|
521
|
+
* `response.archive` straight through), so every field below is a remote peer's assertion, and
|
|
522
|
+
* `saveRestored` writes keyed by REVISION and by ACTION ID — meaning an archive naming a
|
|
523
|
+
* revision or action id this node already holds would otherwise overwrite content that was never
|
|
524
|
+
* in question. The checks, in order:
|
|
525
|
+
*
|
|
526
|
+
* - **The archive is about this block.** `saveRestored` writes under `this.blockId` and ignores
|
|
527
|
+
* `archive.blockId`, so an answer about a different block would land as this block's history.
|
|
528
|
+
* - **Every revision key is a real revision.** Keys arrive as JSON strings; a non-numeric key
|
|
529
|
+
* coerces to `NaN` and would be stored as a garbage revision number. Min/max are folded rather
|
|
530
|
+
* than spread through `Math.min`/`Math.max`, which throws `RangeError` past ~125k arguments —
|
|
531
|
+
* reachable inside the 8 MiB sync-response cap (see `maxArchiveRevision`, same hazard).
|
|
532
|
+
* - **Each entry's own `rev`, when it declares one, agrees with the key it is filed under.**
|
|
533
|
+
* That disagreement IS the mislabel this ticket's family of bugs is about, in miniature.
|
|
534
|
+
* - **The archive answers the pin.** NOT "carries revision `rev`" — `ActionContext.rev` is a
|
|
535
|
+
* COLLECTION-wide revision, so it routinely sits above the revision at which this particular
|
|
536
|
+
* block last changed. A peer answering a pin at 9 for a block whose last commit was rev 2
|
|
537
|
+
* correctly serves rev 2, labelled as rev 2 (pinned in `test/block-archive-proof.spec.ts`).
|
|
538
|
+
* So the rule is that the archive's LOWEST revision is at or below the pin: `materializeBlock`
|
|
539
|
+
* descends from `rev`, so an archive entirely above the pin answers a different question and
|
|
540
|
+
* is exactly the "old bytes under a newer label" shape that overwrites good local data.
|
|
541
|
+
* - **The declared `range` agrees with the revisions actually carried** — it starts at the
|
|
542
|
+
* lowest (the floor must be present, or the descending walk has nothing to land on) and ends
|
|
543
|
+
* past the highest. An OPEN-ENDED range is refused outright: it would claim infinite coverage
|
|
544
|
+
* and permanently disable restore for this block on one unverified peer's say-so.
|
|
545
|
+
* (`RestoreCallback` allows open-ended for the UNPINNED call; `restoreRevision` never makes one.)
|
|
546
|
+
* - **Nothing already held is overwritten with different content** — see
|
|
547
|
+
* {@link noDivergentRewrite}.
|
|
548
|
+
*
|
|
549
|
+
* ## What gets recorded, and the one thing taken on trust
|
|
550
|
+
*
|
|
551
|
+
* The coverage returned is `[lowest, rev + 1)` — the archive's floor, up to the PIN and no
|
|
552
|
+
* further. Both halves of that are deliberate.
|
|
553
|
+
*
|
|
554
|
+
* Extending UP to the pin is an INFERENCE, and the only one here: a peer
|
|
555
|
+
* answering a pinned fetch with revision M ≤ N means "M is my highest committed revision of this
|
|
556
|
+
* block at or below N", i.e. nothing changed in (M, N]. This node cannot verify that locally.
|
|
557
|
+
*
|
|
558
|
+
* It is recorded anyway because the alternative is worse. `meta.ranges` is what
|
|
559
|
+
* {@link restoreRevision} consults to decide whether to fetch at all, so recording only the
|
|
560
|
+
* archive's literal `[M, M+1)` leaves `inRanges(N)` false and re-runs the ENTIRE restore — network
|
|
561
|
+
* round trip plus a full `saveRestored` write — on every later read at that pin, forever, never
|
|
562
|
+
* converging. The inference is also unavoidable rather than merely convenient: having the peer
|
|
563
|
+
* state the claim on the wire instead would not make it verifiable, only explicit, while breaking
|
|
564
|
+
* repair against every peer running an older build.
|
|
565
|
+
*
|
|
566
|
+
* Stopping AT the pin, on the other hand, discards coverage for any revision the archive
|
|
567
|
+
* volunteered ABOVE it. Those entries are still WRITTEN — an honest peer serves a contiguous
|
|
568
|
+
* span, so an archive of `{2, 3, 4}` answering a pin at 3 is normal, not hostile — they are
|
|
569
|
+
* merely not CLAIMED. `rev` is the one number in the exchange this node chose, so it is as far as
|
|
570
|
+
* its trust in the answer should reach; recording `highest + 1` instead would let the peer set
|
|
571
|
+
* the width of its own credibility by padding the archive with fabricated high revisions, and
|
|
572
|
+
* reads across that padded span would then be served from local content without ever re-asking.
|
|
573
|
+
* The cost is one redundant fetch the first time a revision above the pin is read; that fetch is
|
|
574
|
+
* idempotent (identical content is not a conflict, see {@link noDivergentRewrite}) and the
|
|
575
|
+
* coverage converges.
|
|
576
|
+
*
|
|
577
|
+
* NOTE: accepted tradeoff — a lying peer's answer is therefore STICKY across the whole span it
|
|
578
|
+
* was asked about: reads between M and N are served locally from M's content and never re-ask, so
|
|
579
|
+
* a later honest peer is never consulted for them. Weighed against an unbounded re-fetch loop and
|
|
580
|
+
* kept; that is the same "ranges records what this node can locally reconstruct, freshness is a
|
|
581
|
+
* separate concern" position `setLatest` and `saveForwardRevision` already take. Revisit if a
|
|
582
|
+
* restore ever gains a way to verify an archive (a commit proof chain over the served revision
|
|
583
|
+
* would do it) — at that point record only what verifies.
|
|
584
|
+
*/
|
|
585
|
+
private async vetRestoredArchive(archive: BlockArchive, rev: number): Promise<RevisionRange | undefined> {
|
|
586
|
+
const refuse = (why: string, ...args: unknown[]): undefined => {
|
|
587
|
+
log(`restore:refused blockId=%s rev=%d ${why}`, this.blockId, rev, ...args);
|
|
588
|
+
return undefined;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if (archive.blockId !== this.blockId) {
|
|
592
|
+
return refuse('archive is for blockId=%s', archive.blockId);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const entries: RestoredRevision[] = [];
|
|
596
|
+
let lowest: number | undefined;
|
|
597
|
+
let highest: number | undefined;
|
|
598
|
+
for (const [key, entry] of Object.entries(archive.revisions ?? {})) {
|
|
599
|
+
const entryRev = Number(key);
|
|
600
|
+
// `String(entryRev) === key`, not merely "parses as an integer". `saveRestored` re-derives
|
|
601
|
+
// the number with its own `Number(key)`, so any key with a second spelling (`"02"`, `" 2"`,
|
|
602
|
+
// `"2e1"`) lets ONE archive file two entries under one revision: the vet checks both, the
|
|
603
|
+
// write keeps whichever `Object.entries` yields last, and which one that is was never the
|
|
604
|
+
// question either check answered.
|
|
605
|
+
if (!Number.isInteger(entryRev) || entryRev < 1 || String(entryRev) !== key) {
|
|
606
|
+
return refuse('revision key %s is not a revision', key);
|
|
607
|
+
}
|
|
608
|
+
const action = entry?.action;
|
|
609
|
+
if (!action?.actionId) {
|
|
610
|
+
return refuse('revision %d carries no action', entryRev);
|
|
611
|
+
}
|
|
612
|
+
if (action.rev !== undefined && action.rev !== entryRev) {
|
|
613
|
+
return refuse('revision %d is filed under an action declaring rev=%d', entryRev, action.rev);
|
|
614
|
+
}
|
|
615
|
+
entries.push({ rev: entryRev, action, block: entry.block });
|
|
616
|
+
if (lowest === undefined || entryRev < lowest) lowest = entryRev;
|
|
617
|
+
if (highest === undefined || entryRev > highest) highest = entryRev;
|
|
618
|
+
}
|
|
619
|
+
if (lowest === undefined || highest === undefined) {
|
|
620
|
+
return refuse('carries no revisions');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (lowest > rev) {
|
|
624
|
+
return refuse('lowest revision %d is above the pin', lowest);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const range = archive.range;
|
|
628
|
+
if (!Array.isArray(range)) {
|
|
629
|
+
return refuse('declares no range');
|
|
630
|
+
}
|
|
631
|
+
const [start, end] = range;
|
|
632
|
+
if (start !== lowest) {
|
|
633
|
+
return refuse('range starts at %o but revisions start at %d', start, lowest);
|
|
634
|
+
}
|
|
635
|
+
if (end === undefined || !Number.isInteger(end) || end <= highest) {
|
|
636
|
+
return refuse('range ends at %o but revisions end at %d', end, highest);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!await this.noDivergentRewrite(entries, refuse)) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return [lowest, rev + 1];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* True when none of `entries` would overwrite content this node ALREADY holds with different
|
|
648
|
+
* content. False (having logged which entry, via `refuse`) when any would.
|
|
649
|
+
*
|
|
650
|
+
* The refusal is all-or-nothing: one divergent entry rejects the WHOLE archive rather than
|
|
651
|
+
* landing the entries this node happens to lack. Two reasons. An archive that contradicts locally
|
|
652
|
+
* held content is evidence the peer is wrong or hostile about this block, which makes the rest of
|
|
653
|
+
* it no more trustworthy than the part that was caught; and a partial apply would leave
|
|
654
|
+
* {@link vetRestoredArchive}'s coverage claiming a span the applied subset may not support.
|
|
655
|
+
*
|
|
656
|
+
* Identical content is NOT a conflict — a re-restore of the same archive must stay idempotent,
|
|
657
|
+
* which it has to be for the pin-extended coverage above to converge.
|
|
658
|
+
*
|
|
659
|
+
* NOTE: accepted tradeoff — first writer wins, permanently. Once a revision record is held, no
|
|
660
|
+
* later archive can replace it, so a lying peer that answers a gap FIRST makes every honest
|
|
661
|
+
* archive restating that revision refuse forever: the revision becomes unreadable rather than
|
|
662
|
+
* wrong. That is the deliberate direction — this guard exists precisely so a peer cannot rewrite
|
|
663
|
+
* held history, and it cannot tell "the held copy is the lie" from "the incoming copy is". Repair
|
|
664
|
+
* from that state is an operator action (drop the block's local records and re-fetch). Revisit
|
|
665
|
+
* only alongside a way to verify an archive, which would let the guard prefer the provable copy
|
|
666
|
+
* instead of the earlier one.
|
|
667
|
+
*
|
|
668
|
+
* Comparison is by `canonicalJson`, db-core's one deterministic encoding, so key ORDER across a
|
|
669
|
+
* JSON round trip over the wire never reads as divergence.
|
|
670
|
+
*
|
|
671
|
+
* The three keys mirror {@link saveRestored}'s three writes exactly; an entry that carries no
|
|
672
|
+
* `block` writes no materialization, so it cannot clobber one and is not checked for it.
|
|
673
|
+
*
|
|
674
|
+
* NOTE: costs up to three raw-storage reads per revision entry, on the restore path only — which
|
|
675
|
+
* has already paid for a network round trip, so it is not the term that matters. If a restore
|
|
676
|
+
* ever carries thousands of revisions and this shows up, check `getRevision` first and skip the
|
|
677
|
+
* other two for a revision this node does not hold at all.
|
|
678
|
+
*/
|
|
679
|
+
private async noDivergentRewrite(
|
|
680
|
+
entries: RestoredRevision[],
|
|
681
|
+
refuse: (why: string, ...args: unknown[]) => undefined
|
|
682
|
+
): Promise<boolean> {
|
|
683
|
+
for (const { rev, action, block } of entries) {
|
|
684
|
+
const heldActionId = await this.storage.getRevision(this.blockId, rev);
|
|
685
|
+
if (heldActionId !== undefined && heldActionId !== action.actionId) {
|
|
686
|
+
refuse('revision %d is already held as action %s, archive names %s',
|
|
687
|
+
rev, heldActionId, action.actionId);
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const heldTransform = await this.storage.getTransaction(this.blockId, action.actionId);
|
|
692
|
+
if (heldTransform !== undefined && canonicalJson(heldTransform) !== canonicalJson(action.transform)) {
|
|
693
|
+
refuse('action %s (revision %d) is already held with a different transform', action.actionId, rev);
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (block) {
|
|
698
|
+
const heldBlock = await this.storage.getMaterializedBlock(this.blockId, action.actionId);
|
|
699
|
+
if (heldBlock !== undefined && canonicalJson(heldBlock) !== canonicalJson(block)) {
|
|
700
|
+
refuse('action %s (revision %d) is already materialized with different content', action.actionId, rev);
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Persist a fetched archive's revisions locally.
|
|
710
|
+
*
|
|
711
|
+
* A revision entry's own `proof` is deliberately IGNORED. An archive is remote wire data —
|
|
712
|
+
* {@link restoreBlock}'s `RestorationCoordinator` fetch verifies nothing, and a peer chooses
|
|
713
|
+
* what to attach — so persisting a proof read out of the archive body would re-serve a hostile
|
|
714
|
+
* peer's artifact as evidence this node retained itself. A proof reaches storage only through
|
|
715
|
+
* `verified`, passed out-of-band alongside the archive, which exactly one caller chain supplies:
|
|
716
|
+
* `cluster/reconcile-block.ts` → `StorageRepo.saveReplicatedBlock` → {@link saveReplica} →
|
|
717
|
+
* {@link saveForwardRevision}, where `certifyContent` had already bound the proof to these exact
|
|
718
|
+
* bytes. A separate parameter rather than a caller obligation to strip is what makes "an
|
|
719
|
+
* unverified proof reached `saveBlockProof`" unrepresentable instead of merely documented.
|
|
720
|
+
*
|
|
721
|
+
* This is a WRITER, not a gate: it trusts what it is handed, and each of its two callers is
|
|
722
|
+
* responsible for having earned that on its own terms.
|
|
723
|
+
*
|
|
724
|
+
* - {@link restoreRevision} — the unverified restore wire — runs {@link vetRestoredArchive}
|
|
725
|
+
* first. Those checks are ABOUT the pinned request (does the archive answer the revision that
|
|
726
|
+
* was asked for?), and this function has no pin to check against, so they cannot live here.
|
|
727
|
+
* - {@link saveForwardRevision} — reached by `saveReplica`/`saveDeletion` through
|
|
728
|
+
* `StorageRepo.saveReplicatedBlock` — builds the archive it passes from local arguments, and
|
|
729
|
+
* on the replica path `cluster/reconcile-block.ts` has already bound those bytes to a verified
|
|
730
|
+
* proof. It writes strictly ABOVE its own `latest` (the monotonic guard returns first
|
|
731
|
+
* otherwise), so it cannot rewrite held history, and it deliberately pays nothing for the
|
|
732
|
+
* restore wire's checks.
|
|
733
|
+
*
|
|
734
|
+
* A THIRD caller would not inherit either argument. Any future one that takes an archive off a
|
|
735
|
+
* network must route through `vetRestoredArchive` (or an equivalent for its own trust model)
|
|
736
|
+
* before reaching here.
|
|
737
|
+
*/
|
|
738
|
+
private async saveRestored(archive: BlockArchive, verified?: { rev: number; proof: BlockCommitProof }) {
|
|
739
|
+
const revisions = Object.entries(archive.revisions)
|
|
740
|
+
.map(([rev, data]) => ({ rev: Number(rev), data }));
|
|
741
|
+
|
|
742
|
+
// Save all revisions, actions, materializations, and the caller-verified proof (if any).
|
|
743
|
+
for (const { rev, data: { action, block } } of revisions) {
|
|
744
|
+
await Promise.all([
|
|
745
|
+
this.storage.saveRevision(this.blockId, rev, action.actionId),
|
|
746
|
+
this.storage.saveTransaction(this.blockId, action.actionId, action.transform),
|
|
747
|
+
block ? this.storage.saveMaterializedBlock(this.blockId, action.actionId, block) : Promise.resolve(),
|
|
748
|
+
verified?.rev === rev ? this.storage.saveBlockProof(this.blockId, rev, verified.proof) : Promise.resolve()
|
|
749
|
+
]);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private inRanges(rev: number, ranges: RevisionRange[]): boolean {
|
|
754
|
+
return ranges.some(range =>
|
|
755
|
+
rev >= range[0] && (range[1] === undefined || rev < range[1])
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Checkpoint retention predicate. A materialization at `rev` must be kept iff it is the tip
|
|
761
|
+
* (`latestRev` — the common read target and the replay base for the next commit), the floor of its
|
|
762
|
+
* contiguous range (`rangeFloor` — the descending walk in {@link materializeBlock} has nothing below
|
|
763
|
+
* the floor to fall back to), or a periodic checkpoint (`rev % checkpointInterval === 0`, which bounds
|
|
764
|
+
* replay depth). Otherwise the materialization is prunable — its forward transform is retained, so the
|
|
765
|
+
* rev stays reconstructible by replay from the nearest retained materialization below it. The floor
|
|
766
|
+
* clause is SEPARATE and mandatory: absolute `rev % K` checkpoints do not automatically land on the
|
|
767
|
+
* floor (e.g. floor `E = 1`, `K = 32`).
|
|
768
|
+
*/
|
|
769
|
+
private isRetainedRev(rev: number, latestRev: number, rangeFloor: number): boolean {
|
|
770
|
+
return rev === latestRev
|
|
771
|
+
|| rev === rangeFloor
|
|
772
|
+
|| rev % this.checkpointInterval === 0;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Start of the contiguous `ranges` span containing `rev`. Falls back to `rev` itself when no span
|
|
776
|
+
* contains it — unreachable for a committed rev (setLatest always merges the containing span before a
|
|
777
|
+
* prune/read runs), and the conservative direction (treats `rev` as its own floor ⇒ retained). */
|
|
778
|
+
private rangeFloorOf(rev: number, ranges: RevisionRange[]): number {
|
|
779
|
+
for (const range of ranges) {
|
|
780
|
+
const [start, end] = range;
|
|
781
|
+
if (rev >= start && (end === undefined || rev < end)) {
|
|
782
|
+
return start;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return rev;
|
|
786
|
+
}
|
|
787
|
+
}
|