@optimystic/db-p2p 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{readme.md → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,203 +1,404 @@
|
|
|
1
|
-
import type { ActionRev, BlockId, IBlock } from "@optimystic/db-core";
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1
|
+
import type { ActionId, ActionRev, BlockId, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import { canonicalBlockHash } from "@optimystic/db-core";
|
|
3
|
+
import type { BlockArchive } from "../storage/struct.js";
|
|
4
|
+
import { maxArchiveRevision } from "../storage/block-archive.js";
|
|
5
|
+
import type { ReconcileBlockCallback } from "./cluster-repo.js";
|
|
6
|
+
import type { IPeerReputation } from "../reputation/types.js";
|
|
7
|
+
import { PenaltyReason } from "../reputation/types.js";
|
|
8
|
+
import {
|
|
9
|
+
selectQuorumRev, selectQuorumBlock, corroboratorCapacity, quorumSize,
|
|
10
|
+
certifiedEquivocation, certifiedContentEquivocation,
|
|
11
|
+
type RevClaim, type BlockHashCandidate, type QuorumRev
|
|
12
|
+
} from "./quorum-restore.js";
|
|
13
|
+
import {
|
|
14
|
+
certifyClaim, certifyContent, isAttributableProofFailure, proofThresholds, type ProofAnchoring
|
|
15
|
+
} from "./certified-claims.js";
|
|
16
|
+
import type { BlockCommitProof } from "./commit-proof.js";
|
|
17
|
+
import { createLogger } from '../logger.js';
|
|
18
|
+
|
|
19
|
+
const log = createLogger('reconcile-block');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wall-clock bound on one whole reconcile pass (all cohort peers, both quorums, the persist).
|
|
23
|
+
* Shared by both callers so a slow or unreachable cohort peer stalls neither the commit path
|
|
24
|
+
* (`ClusterMember.withReconcileTimeout` — a stall there holds up consensus execution) nor the read
|
|
25
|
+
* path (`CoordinatorRepo.restoreCorroborated` — a stall there holds up a caller's `get`).
|
|
26
|
+
*/
|
|
27
|
+
export const RECONCILE_TIMEOUT_MS = 5000;
|
|
28
|
+
|
|
29
|
+
/** One cohort peer's answer for a block: its highest revision, and the block bytes if it carried them. */
|
|
30
|
+
interface ReconcileCandidate {
|
|
31
|
+
peerId: string;
|
|
32
|
+
rev: number;
|
|
33
|
+
actionId: string;
|
|
34
|
+
/** Present only when the serving archive carried a materialized block for `rev`. */
|
|
35
|
+
block?: IBlock;
|
|
36
|
+
/**
|
|
37
|
+
* The cohort commit proof the serving archive attached, when it carried one. The archive keys the
|
|
38
|
+
* proof INSIDE the same revision entry as the `(rev, actionId)` it certifies
|
|
39
|
+
* (`storage/struct.ts` `ArchiveRevisions`), so a serving peer cannot pair a genuine proof with a
|
|
40
|
+
* different revision — mis-pairing is structurally blocked by the wire shape. Presence proves
|
|
41
|
+
* nothing; the certification pass below is what turns it into a verdict.
|
|
42
|
+
*/
|
|
43
|
+
proof?: BlockCommitProof;
|
|
44
|
+
/** Injected by the certification pass: the proof verified for this `(rev, actionId)`. Never set from mere proof presence. */
|
|
45
|
+
revCertified?: boolean;
|
|
46
|
+
/** Injected by the certification pass: the proof's declared digest matches these exact bytes. */
|
|
47
|
+
contentCertified?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Set on `digest-mismatch` only: the served bytes provably contradict the proof's declared
|
|
50
|
+
* digest, so this candidate is dropped from the content quorum (its rev claim still counts —
|
|
51
|
+
* that half genuinely verified).
|
|
52
|
+
*/
|
|
53
|
+
contentRejected?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Collaborators {@link createReconcileBlock} needs, injected so the logic stays transport-agnostic. */
|
|
57
|
+
export interface ReconcileBlockDeps {
|
|
58
|
+
/** This node's own peer id; excluded from the cohort targets. */
|
|
59
|
+
selfPeerId: string;
|
|
60
|
+
/** Fetch one cohort peer's archive for `blockId` — `undefined` when it is unreachable or holds nothing. */
|
|
61
|
+
fetchArchive: (peerId: string, blockId: BlockId) => Promise<BlockArchive | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* Persist the agreed content through the churn-replication funnel. `verifiedProof` is passed
|
|
64
|
+
* ONLY when it was verified against these exact bytes (`certifyContent`'s digest check) — the
|
|
65
|
+
* receiver persists it as evidence, so an unverified proof must never reach this parameter.
|
|
66
|
+
*/
|
|
67
|
+
saveReplicatedBlock: (blockId: BlockId, block: IBlock, source: ActionRev, verifiedProof?: BlockCommitProof) => Promise<void>;
|
|
68
|
+
/** Proportional corroboration threshold; the cohort's `simpleMajorityThreshold`. */
|
|
69
|
+
simpleMajorityThreshold: number;
|
|
70
|
+
/** Promise-round gate for proof verification; the cohort's `superMajorityThreshold`. */
|
|
71
|
+
superMajorityThreshold: number;
|
|
72
|
+
/** Optional layer-2 anchoring for accepted proofs — observational only, see `cluster/certified-claims.ts`. */
|
|
73
|
+
anchoring?: ProofAnchoring;
|
|
74
|
+
/**
|
|
75
|
+
* Yardstick the corroboration floor is measured against — the floor for
|
|
76
|
+
* {@link corroboratorCapacity}. Required, not optional: unlike the membership admission gate there
|
|
77
|
+
* is no "unknown" handling here, so a caller that cannot state an asserted cohort size should pass
|
|
78
|
+
* its configured `clusterSize` (the strict direction) rather than a small placeholder. The failure
|
|
79
|
+
* mode of overstating it is a block that stays unrepaired — degraded, not dead; of understating it,
|
|
80
|
+
* a shrunken cohort view that can relax the floor to a single voter. `resolveClusterPolicy`
|
|
81
|
+
* (`cluster/cluster-policy.ts`) resolves it for a real node and defaults it to `clusterSize`.
|
|
82
|
+
*/
|
|
83
|
+
repairCorroborationClusterSize: number;
|
|
84
|
+
/** Best-effort misbehavior reporting; a throwing implementation is swallowed. */
|
|
85
|
+
reputation?: Pick<IPeerReputation, 'reportPeer'>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What one cohort peer's fetch produced. Kept as separate outcomes rather than collapsed to
|
|
90
|
+
* `ReconcileCandidate | undefined` so a decline can report WHICH populations it was short of: "1 of 3
|
|
91
|
+
* responded" and "1 holder, 2 confirmed non-holders" call for completely different operator actions.
|
|
92
|
+
*
|
|
93
|
+
* NOTE: `no-archive` deliberately conflates "the peer holds nothing" with "the peer is unreachable" —
|
|
94
|
+
* that conflation is in `fetchArchive`'s contract, and the production wiring
|
|
95
|
+
* (`libp2p-node-base.fetchArchiveFromPeer`) swallows every dial failure and timeout into the same
|
|
96
|
+
* `undefined`. The read path does separate the two (`CoordinatorRepo`'s `silent` set). If a
|
|
97
|
+
* reconcile-side decline ever needs that distinction, widen `fetchArchive` to report unreachability
|
|
98
|
+
* rather than trying to infer it here.
|
|
99
|
+
*/
|
|
100
|
+
type PeerAnswer =
|
|
101
|
+
/** Served an archive covering the committed revision or better. */
|
|
102
|
+
| { kind: 'claim'; candidate: ReconcileCandidate }
|
|
103
|
+
/** Served an archive, but it covers nothing at or above the revision being healed. */
|
|
104
|
+
| { kind: 'behind' }
|
|
105
|
+
/** Served no archive: holds nothing, or is unreachable — see the note above. */
|
|
106
|
+
| { kind: 'no-archive' }
|
|
107
|
+
/** The fetch threw. */
|
|
108
|
+
| { kind: 'error' };
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The claim a peer's archive makes: its highest revision, provided that revision is at least the
|
|
112
|
+
* one we committed. `undefined` when the archive carries only revisions older than the commit we are
|
|
113
|
+
* healing, or no usable action at its highest.
|
|
114
|
+
*/
|
|
115
|
+
function toCandidate(peerId: string, archive: BlockArchive, committedRev: number): ReconcileCandidate | undefined {
|
|
116
|
+
const maxRev = maxArchiveRevision(archive.revisions);
|
|
117
|
+
if (maxRev === undefined || maxRev < committedRev) return undefined;
|
|
118
|
+
const data = archive.revisions[maxRev];
|
|
119
|
+
if (!data?.action) return undefined;
|
|
120
|
+
return {
|
|
121
|
+
peerId, rev: maxRev, actionId: data.action.actionId, block: data.block,
|
|
122
|
+
...(data.proof ? { proof: data.proof } : {})
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* One peer's answer, isolated. `fetchArchive` is contracted to answer `undefined` for an
|
|
128
|
+
* unreachable peer, but a raw `Promise.all` over the cohort would let a single rejecting fetch
|
|
129
|
+
* discard the answers every other peer already gave — turning a heal the cohort could complete
|
|
130
|
+
* into a decline. One peer's failure costs only that peer's vote.
|
|
131
|
+
*/
|
|
132
|
+
async function fetchAnswer(
|
|
133
|
+
deps: ReconcileBlockDeps,
|
|
134
|
+
peerId: string,
|
|
135
|
+
blockId: BlockId,
|
|
136
|
+
committedRev: number
|
|
137
|
+
): Promise<PeerAnswer> {
|
|
138
|
+
let archive: BlockArchive | undefined;
|
|
139
|
+
try {
|
|
140
|
+
archive = await deps.fetchArchive(peerId, blockId);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log('reconcile:fetch-error', { blockId, peerId, error: (err as Error).message });
|
|
143
|
+
return { kind: 'error' };
|
|
144
|
+
}
|
|
145
|
+
if (!archive) return { kind: 'no-archive' };
|
|
146
|
+
const candidate = toCandidate(peerId, archive, committedRev);
|
|
147
|
+
return candidate ? { kind: 'claim', candidate } : { kind: 'behind' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run every proof-carrying candidate through the shared certification layer
|
|
152
|
+
* (`cluster/certified-claims.ts`) and inject the verdicts, BEFORE selection reads the claim set.
|
|
153
|
+
* Mirrors the read path's certifyClaim pass (`CoordinatorRepo.queryClusterForLatest`) — same
|
|
154
|
+
* thresholds, same failure logging, same penalty discipline:
|
|
155
|
+
*
|
|
156
|
+
* - A candidate whose claim half fails stays an ORDINARY uncertified corroborator (its vote is
|
|
157
|
+
* never dropped — a peer that could fabricate a bad proof could equally have sent none), and
|
|
158
|
+
* only an attributable failure (`isAttributableProofFailure`) penalizes the serving peer.
|
|
159
|
+
* - `digest-mismatch` is the one content-side rejection: the served bytes provably contradict
|
|
160
|
+
* the proof's declared digest, so the candidate is dropped from the content quorum and the
|
|
161
|
+
* server penalized — while its (genuinely verified) rev claim still counts.
|
|
162
|
+
* - `no-digest-declared` leaves the content an ordinary uncertified carrier: the cohort declared
|
|
163
|
+
* nothing to compare against, which is a verdict, never misbehavior.
|
|
164
|
+
*
|
|
165
|
+
* NOTE: cost is one verification pass per proof-carrying answer per reconcile, each bounded by
|
|
166
|
+
* MAX_PROOF_SIGNERS (256, the shared layer's cap) signature checks × cohort width — fine at
|
|
167
|
+
* deployment cohort sizes (~10).
|
|
168
|
+
*/
|
|
169
|
+
async function certifyCandidates(deps: ReconcileBlockDeps, blockId: BlockId, candidates: ReconcileCandidate[]): Promise<void> {
|
|
170
|
+
// Shared with the read path, so the two cannot drift on what the members actually enforced —
|
|
171
|
+
// see `proofThresholds` for why the simple-majority term is not deps.simpleMajorityThreshold.
|
|
172
|
+
const thresholds = proofThresholds(deps.superMajorityThreshold);
|
|
173
|
+
await Promise.all(candidates.map(async c => {
|
|
174
|
+
if (!c.proof) return;
|
|
175
|
+
const claim = { blockId, rev: c.rev, actionId: c.actionId as ActionId };
|
|
176
|
+
if (c.block) {
|
|
177
|
+
const verdict = await certifyContent(c.proof, claim, c.block, thresholds, deps.anchoring);
|
|
178
|
+
if (verdict.revCertified) {
|
|
179
|
+
c.revCertified = true;
|
|
180
|
+
if (verdict.contentCertified) {
|
|
181
|
+
c.contentCertified = true;
|
|
182
|
+
} else if (verdict.failure === 'digest-mismatch') {
|
|
183
|
+
// The claim half genuinely passed; the bytes provably lie. Drop them from the
|
|
184
|
+
// content quorum and penalize the server — repair continues on the other holders.
|
|
185
|
+
c.contentRejected = true;
|
|
186
|
+
log('reconcile:content-rejected', { blockId, peerId: c.peerId, rev: c.rev });
|
|
187
|
+
penalizeProofService(deps.reputation, c.peerId, blockId);
|
|
188
|
+
}
|
|
189
|
+
// no-digest-declared: content stays an ordinary uncertified carrier, no penalty.
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
logUncertified(blockId, c, verdict.failure);
|
|
193
|
+
if (isAttributableProofFailure(verdict.failure)) {
|
|
194
|
+
penalizeProofService(deps.reputation, c.peerId, blockId);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const verdict = await certifyClaim(c.proof, claim, thresholds, deps.anchoring);
|
|
199
|
+
if (verdict.certified) {
|
|
200
|
+
c.revCertified = true;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
logUncertified(blockId, c, verdict.failure);
|
|
204
|
+
if (isAttributableProofFailure(verdict.failure)) {
|
|
205
|
+
penalizeProofService(deps.reputation, c.peerId, blockId);
|
|
206
|
+
}
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Mirror of `cluster-fetch:proof-uncertified` on the read path — same fields, reconcile-side name. */
|
|
211
|
+
function logUncertified(blockId: BlockId, c: ReconcileCandidate, failure: string): void {
|
|
212
|
+
log('reconcile:proof-uncertified', { blockId, peerId: c.peerId, rev: c.rev, failure });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Best-effort penalty for a peer whose served proof (or the bytes served under it) provably lies.
|
|
217
|
+
* Mirrors `CoordinatorRepo.penalizeProofService` — never throws.
|
|
218
|
+
*/
|
|
219
|
+
function penalizeProofService(
|
|
220
|
+
reputation: Pick<IPeerReputation, 'reportPeer'> | undefined, peerId: string, blockId: BlockId
|
|
221
|
+
): void {
|
|
222
|
+
if (!reputation) return;
|
|
223
|
+
try {
|
|
224
|
+
reputation.reportPeer(peerId, PenaltyReason.InvalidRestoration, `reconcile:${blockId}`);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
log('reconcile:penalize-error', { blockId, error: (err as Error).message });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Hash the block bytes of every candidate that both corroborates `selected` and actually carried
|
|
232
|
+
* content — minus any whose bytes were rejected against their own proof's digest
|
|
233
|
+
* (`contentRejected`). A carrier whose content CERTIFIED carries its `certified` flag into the
|
|
234
|
+
* selector and its proof alongside: that is the only proof safe to persist, because it was
|
|
235
|
+
* verified against these exact bytes.
|
|
236
|
+
*
|
|
237
|
+
* NOTE: this canonical-JSON-serializes and sha256s every carrier's whole block on every reconcile.
|
|
238
|
+
* Negligible at today's cohort widths and block sizes; if blocks grow large or cohorts wide enough
|
|
239
|
+
* for this to show up on a commit-path profile, hash incrementally at receive time instead.
|
|
240
|
+
*/
|
|
241
|
+
async function hashCarriers(
|
|
242
|
+
candidates: ReconcileCandidate[], selected: QuorumRev
|
|
243
|
+
): Promise<(BlockHashCandidate & { proof?: BlockCommitProof })[]> {
|
|
244
|
+
const carriers = candidates.filter(c =>
|
|
245
|
+
c.rev === selected.rev && c.actionId === selected.actionId && c.block && !c.contentRejected);
|
|
246
|
+
return await Promise.all(
|
|
247
|
+
carriers.map(async c => ({
|
|
248
|
+
peerId: c.peerId, hash: await canonicalBlockHash(c.block!), block: c.block!,
|
|
249
|
+
...(c.contentCertified ? { certified: true } : {}),
|
|
250
|
+
...(c.contentCertified && c.proof ? { proof: c.proof } : {})
|
|
251
|
+
}))
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Report cohort members that served content contradicting the agreed hash. Best-effort; never throws. */
|
|
256
|
+
function penalizeContradictingContent(
|
|
257
|
+
reputation: Pick<IPeerReputation, 'reportPeer'> | undefined,
|
|
258
|
+
candidates: BlockHashCandidate[],
|
|
259
|
+
agreedHash: string,
|
|
260
|
+
blockId: BlockId
|
|
261
|
+
): void {
|
|
262
|
+
if (!reputation) return;
|
|
263
|
+
try {
|
|
264
|
+
for (const c of candidates) {
|
|
265
|
+
if (c.hash !== agreedHash) {
|
|
266
|
+
reputation.reportPeer(c.peerId, PenaltyReason.InvalidRestoration, `reconcile:${blockId}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
log('reconcile:penalize-error', { blockId, error: (err as Error).message });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Active reconciliation for a block this member committed without a materializable base
|
|
276
|
+
* (cohort drift between the independent pend and commit cluster-transactions, or a refused
|
|
277
|
+
* `missing-base-revision` commit). Queries the commit cohort — self already excluded by
|
|
278
|
+
* `ClusterMember.reconcileDivergentCommit` — for the block, picks the target revision by quorum
|
|
279
|
+
* corroboration rather than raw `Math.max` (a lone peer inflating its rev cannot steer
|
|
280
|
+
* reconciliation), verifies the cohort agrees on the *content* at that revision, and persists it.
|
|
281
|
+
*
|
|
282
|
+
* Peer-attached cohort commit proofs are verified first ({@link certifyCandidates}) and both gates
|
|
283
|
+
* weigh the verdicts: a claim — and, separately, the bytes — that a verified proof certifies is
|
|
284
|
+
* accepted with no second peer at any cohort size, and the proof that certified the bytes is
|
|
285
|
+
* persisted alongside them so the repaired replica serves it onward.
|
|
286
|
+
*
|
|
287
|
+
* Both quorums are capped by {@link corroboratorCapacity} for the claims that still need
|
|
288
|
+
* corroboration: demanding two corroborators from a cohort that contains exactly one other peer is
|
|
289
|
+
* a permanent deadlock, not a safety property — the node can never heal and stays unreadable
|
|
290
|
+
* forever.
|
|
291
|
+
*
|
|
292
|
+
* **Exposure at capacity 1 (documented, not accidental).** Block ids are random 256-bit strings
|
|
293
|
+
* (`db-core` `structs.ts`), NOT content-addressed, so nothing on the receive path can re-derive
|
|
294
|
+
* the id from the bytes: `canonicalBlockHash` is a cross-peer *agreement* hash, never a check
|
|
295
|
+
* against `blockId`. A sole cohort peer's content is therefore believed on its word. That adds no
|
|
296
|
+
* trust the cohort had not already extended — the same peer's `(rev, actionId)` claim is likewise
|
|
297
|
+
* uncorroborable at that size (see `selectQuorumRev`'s capacity note), and a two-member cohort has
|
|
298
|
+
* no honest majority to appeal to in the first place. It is closed for a candidate that carries a
|
|
299
|
+
* verified cohort commit proof — {@link certifyCandidates} binds the proof's declared digest to the
|
|
300
|
+
* served bytes, which is a check against the *cohort's own signatures* rather than against other
|
|
301
|
+
* peers, so a certified carrier wins the content gate outright and this exposure never applies to
|
|
302
|
+
* it. It remains open for a proof-less candidate, and for the residual that layer 1 proves only
|
|
303
|
+
* that the listed signers signed (`feat-cluster-membership-threshold-cert-anchoring`).
|
|
304
|
+
*
|
|
305
|
+
* Declines are cheap and retryable: nothing is persisted, nothing is marked, and the next commit
|
|
306
|
+
* or churn/rebalance pass tries again.
|
|
307
|
+
*/
|
|
308
|
+
export function createReconcileBlock(deps: ReconcileBlockDeps): ReconcileBlockCallback {
|
|
309
|
+
return async (blockId, committed, cohortPeerIds) => {
|
|
310
|
+
const targets = cohortPeerIds.filter(id => id !== deps.selfPeerId);
|
|
311
|
+
if (targets.length === 0) return;
|
|
312
|
+
|
|
313
|
+
const answers = await Promise.all(
|
|
314
|
+
targets.map(peerId => fetchAnswer(deps, peerId, blockId, committed.rev))
|
|
315
|
+
);
|
|
316
|
+
const candidates = answers.flatMap(a => a.kind === 'claim' ? [a.candidate] : []);
|
|
317
|
+
const tally = (kind: PeerAnswer['kind']) => answers.filter(a => a.kind === kind).length;
|
|
318
|
+
const capacity = corroboratorCapacity(targets.length, deps.repairCorroborationClusterSize);
|
|
319
|
+
|
|
320
|
+
await certifyCandidates(deps, blockId, candidates);
|
|
321
|
+
|
|
322
|
+
const revClaims: RevClaim[] = candidates.map(({ peerId, rev, actionId, revCertified }) => ({
|
|
323
|
+
peerId, rev, actionId, ...(revCertified ? { certified: true } : {})
|
|
324
|
+
}));
|
|
325
|
+
const selected = selectQuorumRev(revClaims, deps.simpleMajorityThreshold, capacity);
|
|
326
|
+
if (!selected) {
|
|
327
|
+
// Asked ONLY on a decline: a certified-vs-certified conflict can coexist with a successful
|
|
328
|
+
// selection (a corroborated pair strictly above the top certified rev wins), so a
|
|
329
|
+
// non-undefined answer here explains THIS decline, nothing more. Two verified proofs for
|
|
330
|
+
// distinct actions at one revision means the cohort (or whoever holds its keys) provably
|
|
331
|
+
// signed both sides — an incident, not a shortage of answers; neither claimant is
|
|
332
|
+
// penalized, because which side is wrong is exactly what this node cannot know.
|
|
333
|
+
const equivocation = certifiedEquivocation(revClaims);
|
|
334
|
+
if (equivocation) {
|
|
335
|
+
log('reconcile:certified-equivocation', {
|
|
336
|
+
blockId, rev: equivocation.rev, actionIds: equivocation.actionIds
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
// Leave the block behind; churn/rebalance and the next commit retry.
|
|
340
|
+
log('reconcile:no-rev-quorum', {
|
|
341
|
+
blockId,
|
|
342
|
+
rev: committed.rev,
|
|
343
|
+
cohortPeers: targets.length,
|
|
344
|
+
holders: revClaims.length,
|
|
345
|
+
behind: tally('behind'),
|
|
346
|
+
noArchive: tally('no-archive'),
|
|
347
|
+
fetchErrors: tally('error'),
|
|
348
|
+
required: quorumSize(revClaims.length, deps.simpleMajorityThreshold, capacity),
|
|
349
|
+
repairCorroborationClusterSize: deps.repairCorroborationClusterSize
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (selected.certified) {
|
|
355
|
+
// Which rule won matters when reading a repair log: a certified selection may rest on a
|
|
356
|
+
// SINGLE claimant whose corroboration is the cohort's signature set, not other voters.
|
|
357
|
+
log('reconcile:certified-selected', {
|
|
358
|
+
blockId, rev: selected.rev, claimants: selected.supporters.length
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const hashCandidates = await hashCarriers(candidates, selected);
|
|
363
|
+
const agreed = selectQuorumBlock(hashCandidates, deps.simpleMajorityThreshold, capacity);
|
|
364
|
+
if (!agreed) {
|
|
365
|
+
// Two certified hashes at one (rev, actionId) means the cohort's keys signed two digests
|
|
366
|
+
// into one revision — a provable compromise an operator must be able to tell apart from a
|
|
367
|
+
// routine carrier shortfall; without this line the two declines log identically.
|
|
368
|
+
const equivocation = certifiedContentEquivocation(hashCandidates);
|
|
369
|
+
if (equivocation) {
|
|
370
|
+
log('reconcile:certified-content-equivocation', {
|
|
371
|
+
blockId, rev: selected.rev, hashes: equivocation.hashes
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
log('reconcile:no-content-quorum', {
|
|
375
|
+
blockId,
|
|
376
|
+
rev: selected.rev,
|
|
377
|
+
carriers: hashCandidates.length,
|
|
378
|
+
required: quorumSize(hashCandidates.length, deps.simpleMajorityThreshold, capacity),
|
|
379
|
+
repairCorroborationClusterSize: deps.repairCorroborationClusterSize
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Detection is sound: selectQuorumBlock's certified branch fires iff exactly one distinct
|
|
385
|
+
// certified hash exists, so a defined `agreed` with a certified carrier at `agreed.hash` ⇔
|
|
386
|
+
// the certified rule won.
|
|
387
|
+
const certifiedCarrier = hashCandidates.find(c => c.certified === true && c.hash === agreed.hash);
|
|
388
|
+
|
|
389
|
+
// Contradicting-content penalties run ONLY on a corroborated win, mirroring the read path's
|
|
390
|
+
// rule (CoordinatorRepo.penalizeContradictingRevClaims): an unanchored proof must not be able
|
|
391
|
+
// to convict the honest cohort — anyone holding N keys can mint a proof that verifies, and
|
|
392
|
+
// penalizing dissenters against it would hand a forged proof a reputation lever. Revisit when
|
|
393
|
+
// certification is anchored to the block's derived cohort
|
|
394
|
+
// (`feat-cluster-membership-threshold-cert-anchoring`).
|
|
395
|
+
if (!certifiedCarrier) {
|
|
396
|
+
penalizeContradictingContent(deps.reputation, hashCandidates, agreed.hash, blockId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// A corroboration-won heal with no certified carrier persists no proof — today's behavior.
|
|
400
|
+
await deps.saveReplicatedBlock(
|
|
401
|
+
blockId, agreed.block, { actionId: selected.actionId, rev: selected.rev }, certifiedCarrier?.proof);
|
|
402
|
+
log('reconcile:restored', { blockId, rev: selected.rev, actionId: selected.actionId });
|
|
403
|
+
};
|
|
404
|
+
}
|