@secondlayer/shared 7.8.0 → 7.10.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/dist/src/archive/derived-stage-report.d.ts +99 -0
- package/dist/src/archive/derived-stage-report.js +64 -0
- package/dist/src/archive/derived-stage-report.js.map +10 -0
- package/dist/src/archive/disk-guard.d.ts +46 -0
- package/dist/src/archive/disk-guard.js +69 -0
- package/dist/src/archive/disk-guard.js.map +10 -0
- package/dist/src/archive/per-height-digest.d.ts +99 -0
- package/dist/src/archive/per-height-digest.js +194 -0
- package/dist/src/archive/per-height-digest.js.map +11 -0
- package/dist/src/archive/range-digest.d.ts +96 -0
- package/dist/src/archive/semantic-digest-builder.d.ts +1183 -0
- package/dist/src/archive/semantic-digest-builder.js +302 -0
- package/dist/src/archive/semantic-digest-builder.js.map +11 -0
- package/dist/src/archive/semantic-digest.d.ts +131 -0
- package/dist/src/archive/semantic-digest.js +143 -0
- package/dist/src/archive/semantic-digest.js.map +10 -0
- package/dist/src/coverage/index.d.ts +1670 -0
- package/dist/src/coverage/index.js +1483 -0
- package/dist/src/coverage/index.js.map +23 -0
- package/dist/src/db/index.d.ts +105 -1
- package/dist/src/db/index.js +31 -1
- package/dist/src/db/index.js.map +6 -5
- package/dist/src/db/queries/chain-reorgs.d.ts +96 -0
- package/dist/src/db/queries/chain-reorgs.js +30 -1
- package/dist/src/db/queries/chain-reorgs.js.map +6 -5
- package/dist/src/db/queries/contracts.d.ts +96 -0
- package/dist/src/db/queries/instance.d.ts +1127 -0
- package/dist/src/db/queries/instance.js +92 -0
- package/dist/src/db/queries/instance.js.map +10 -0
- package/dist/src/db/queries/integrity.d.ts +96 -0
- package/dist/src/db/queries/subgraph-gaps.d.ts +96 -0
- package/dist/src/db/queries/subgraph-operations.d.ts +96 -0
- package/dist/src/db/queries/subgraphs.d.ts +99 -1
- package/dist/src/db/queries/subgraphs.js +52 -23
- package/dist/src/db/queries/subgraphs.js.map +7 -7
- package/dist/src/db/queries/subscriptions.d.ts +96 -0
- package/dist/src/db/queries/subscriptions.js +47 -31
- package/dist/src/db/queries/subscriptions.js.map +5 -5
- package/dist/src/db/schema.d.ts +97 -1
- package/dist/src/index.d.ts +109 -1
- package/dist/src/index.js +52 -1
- package/dist/src/index.js.map +7 -6
- package/dist/src/node/client.d.ts +11 -1
- package/dist/src/node/client.js +42 -4
- package/dist/src/node/client.js.map +3 -3
- package/dist/src/node/local-client.d.ts +96 -0
- package/dist/src/node/nakamoto.d.ts +22 -7
- package/dist/src/node/nakamoto.js +46 -5
- package/dist/src/node/nakamoto.js.map +3 -3
- package/migrations/0115_instances.ts +33 -0
- package/migrations/0116_coverage_schema.ts +163 -0
- package/package.json +29 -1
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/coverage/constraints.ts
|
|
18
|
+
var STAGE_KINDS = ["raw", "decode", "subgraph", "queue"];
|
|
19
|
+
var NATIVE_CLOCKS = ["block", "cursor", "queue"];
|
|
20
|
+
var REPAIR_MODES = [
|
|
21
|
+
"range_safe",
|
|
22
|
+
"full_reindex",
|
|
23
|
+
"archive_replay",
|
|
24
|
+
"none"
|
|
25
|
+
];
|
|
26
|
+
var RUN_STATUSES = [
|
|
27
|
+
"pending",
|
|
28
|
+
"running",
|
|
29
|
+
"complete",
|
|
30
|
+
"syncing",
|
|
31
|
+
"lagging",
|
|
32
|
+
"gap",
|
|
33
|
+
"stale",
|
|
34
|
+
"failed",
|
|
35
|
+
"unverified_import",
|
|
36
|
+
"unanchored",
|
|
37
|
+
"source_unavailable",
|
|
38
|
+
"out_of_scope",
|
|
39
|
+
"disabled",
|
|
40
|
+
"halted"
|
|
41
|
+
];
|
|
42
|
+
var FAILURE_UNITS = ["block", "range", "cursor", "queue"];
|
|
43
|
+
var FAILURE_CLASSES = [
|
|
44
|
+
"omission",
|
|
45
|
+
"version",
|
|
46
|
+
"digest_mismatch",
|
|
47
|
+
"crash",
|
|
48
|
+
"reorg",
|
|
49
|
+
"source_gap",
|
|
50
|
+
"handler",
|
|
51
|
+
"timeout",
|
|
52
|
+
"unknown"
|
|
53
|
+
];
|
|
54
|
+
var RETRY_STATES = ["open", "retrying", "halted", "resolved"];
|
|
55
|
+
var FAILURE_RETENTION_DAYS = 30;
|
|
56
|
+
function isMember(set, value) {
|
|
57
|
+
return set.includes(value);
|
|
58
|
+
}
|
|
59
|
+
function isStageKind(value) {
|
|
60
|
+
return isMember(STAGE_KINDS, value);
|
|
61
|
+
}
|
|
62
|
+
function isNativeClock(value) {
|
|
63
|
+
return isMember(NATIVE_CLOCKS, value);
|
|
64
|
+
}
|
|
65
|
+
function isRepairMode(value) {
|
|
66
|
+
return isMember(REPAIR_MODES, value);
|
|
67
|
+
}
|
|
68
|
+
function isRunStatus(value) {
|
|
69
|
+
return isMember(RUN_STATUSES, value);
|
|
70
|
+
}
|
|
71
|
+
function isFailureUnit(value) {
|
|
72
|
+
return isMember(FAILURE_UNITS, value);
|
|
73
|
+
}
|
|
74
|
+
function isFailureClass(value) {
|
|
75
|
+
return isMember(FAILURE_CLASSES, value);
|
|
76
|
+
}
|
|
77
|
+
function isRetryState(value) {
|
|
78
|
+
return isMember(RETRY_STATES, value);
|
|
79
|
+
}
|
|
80
|
+
function isNonNegativeInt(value) {
|
|
81
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
82
|
+
}
|
|
83
|
+
function rangeIsOrdered(from, to) {
|
|
84
|
+
return isNonNegativeInt(from) && isNonNegativeInt(to) && from <= to;
|
|
85
|
+
}
|
|
86
|
+
function receiptRetentionHolds(row) {
|
|
87
|
+
if (!isNonNegativeInt(row.block_height))
|
|
88
|
+
return false;
|
|
89
|
+
if (!isNonNegativeInt(row.input_count))
|
|
90
|
+
return false;
|
|
91
|
+
if (row.compacted_at !== null && !row.finalized)
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function failureRangeHolds(row) {
|
|
96
|
+
if (row.from_height !== null && !isNonNegativeInt(row.from_height)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (row.to_height !== null && !isNonNegativeInt(row.to_height)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (row.from_height !== null && row.to_height !== null && row.from_height > row.to_height) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
function failureRetentionHolds(row) {
|
|
108
|
+
if (!isNonNegativeInt(row.retry_count))
|
|
109
|
+
return false;
|
|
110
|
+
if (row.retain_until.getTime() < row.created_at.getTime())
|
|
111
|
+
return false;
|
|
112
|
+
if (row.retry_state === "resolved")
|
|
113
|
+
return row.resolved_at !== null;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function segmentsOverlap(a, b) {
|
|
117
|
+
return a.from_height <= b.to_height && b.from_height <= a.to_height;
|
|
118
|
+
}
|
|
119
|
+
function defaultRetainUntil(createdAt) {
|
|
120
|
+
return new Date(createdAt.getTime() + FAILURE_RETENTION_DAYS * 24 * 60 * 60 * 1000);
|
|
121
|
+
}
|
|
122
|
+
// src/streams-cursor.ts
|
|
123
|
+
var EMPTY_RANGE_EVENT_INDEX_SENTINEL = 2147483647;
|
|
124
|
+
function encodeStreamsCursor(cursor) {
|
|
125
|
+
return `${cursor.block_height}:${cursor.event_index}`;
|
|
126
|
+
}
|
|
127
|
+
function decodeStreamsCursor(cursor) {
|
|
128
|
+
const match = /^(0|[1-9]\d*):(0|[1-9]\d*)$/.exec(cursor);
|
|
129
|
+
if (!match) {
|
|
130
|
+
throw new Error("Invalid Streams cursor");
|
|
131
|
+
}
|
|
132
|
+
const decoded = {
|
|
133
|
+
block_height: Number(match[1]),
|
|
134
|
+
event_index: Number(match[2])
|
|
135
|
+
};
|
|
136
|
+
if (!Number.isSafeInteger(decoded.block_height) || !Number.isSafeInteger(decoded.event_index)) {
|
|
137
|
+
throw new Error("Invalid Streams cursor");
|
|
138
|
+
}
|
|
139
|
+
return decoded;
|
|
140
|
+
}
|
|
141
|
+
function compareStreamsCursor(a, b) {
|
|
142
|
+
if (a.block_height !== b.block_height) {
|
|
143
|
+
return a.block_height < b.block_height ? -1 : 1;
|
|
144
|
+
}
|
|
145
|
+
if (a.event_index !== b.event_index) {
|
|
146
|
+
return a.event_index < b.event_index ? -1 : 1;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
function isEmptyRangeCursor(cursor) {
|
|
151
|
+
return cursor.event_index === EMPTY_RANGE_EVENT_INDEX_SENTINEL;
|
|
152
|
+
}
|
|
153
|
+
function blockEndCursor(height) {
|
|
154
|
+
return {
|
|
155
|
+
block_height: height,
|
|
156
|
+
event_index: EMPTY_RANGE_EVENT_INDEX_SENTINEL
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/coverage/evaluate.ts
|
|
161
|
+
var COVERAGE_REPORT_SCHEMA_VERSION = 1;
|
|
162
|
+
var COVERAGE_STATES = [
|
|
163
|
+
"complete",
|
|
164
|
+
"syncing",
|
|
165
|
+
"lagging",
|
|
166
|
+
"gap",
|
|
167
|
+
"stale",
|
|
168
|
+
"failed",
|
|
169
|
+
"unverified_import",
|
|
170
|
+
"unanchored",
|
|
171
|
+
"source_unavailable",
|
|
172
|
+
"out_of_scope",
|
|
173
|
+
"disabled"
|
|
174
|
+
];
|
|
175
|
+
var HARD_DEP_STATES = new Set([
|
|
176
|
+
"disabled",
|
|
177
|
+
"failed",
|
|
178
|
+
"source_unavailable",
|
|
179
|
+
"out_of_scope",
|
|
180
|
+
"unverified_import",
|
|
181
|
+
"unanchored",
|
|
182
|
+
"gap"
|
|
183
|
+
]);
|
|
184
|
+
var DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 256;
|
|
185
|
+
var DEFAULT_MAX_SOURCE_AGE_SECONDS = 300;
|
|
186
|
+
function mergeRanges(ranges) {
|
|
187
|
+
const sorted = ranges.filter((r) => rangeIsOrdered(r.from_height, r.to_height)).map((r) => ({ from_height: r.from_height, to_height: r.to_height })).sort((a, b) => a.from_height - b.from_height || a.to_height - b.to_height);
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const range of sorted) {
|
|
190
|
+
const last = out[out.length - 1];
|
|
191
|
+
if (last && range.from_height <= last.to_height + 1) {
|
|
192
|
+
last.to_height = Math.max(last.to_height, range.to_height);
|
|
193
|
+
} else {
|
|
194
|
+
out.push(range);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function contiguousThrough(ranges, start) {
|
|
200
|
+
const merged = mergeRanges(ranges);
|
|
201
|
+
let through = null;
|
|
202
|
+
let need = start;
|
|
203
|
+
for (const range of merged) {
|
|
204
|
+
if (range.to_height < need)
|
|
205
|
+
continue;
|
|
206
|
+
if (range.from_height > need)
|
|
207
|
+
break;
|
|
208
|
+
through = range.to_height;
|
|
209
|
+
need = range.to_height + 1;
|
|
210
|
+
}
|
|
211
|
+
return through;
|
|
212
|
+
}
|
|
213
|
+
function findRangeGaps(ranges, from, to) {
|
|
214
|
+
if (to < from)
|
|
215
|
+
return [];
|
|
216
|
+
const merged = mergeRanges(ranges);
|
|
217
|
+
const gaps = [];
|
|
218
|
+
let cursor = from;
|
|
219
|
+
for (const range of merged) {
|
|
220
|
+
if (range.to_height < from)
|
|
221
|
+
continue;
|
|
222
|
+
if (range.from_height > to)
|
|
223
|
+
break;
|
|
224
|
+
if (range.from_height > cursor) {
|
|
225
|
+
gaps.push({
|
|
226
|
+
from_height: cursor,
|
|
227
|
+
to_height: Math.min(range.from_height - 1, to)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
cursor = Math.max(cursor, range.to_height + 1);
|
|
231
|
+
if (cursor > to)
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (cursor <= to) {
|
|
235
|
+
gaps.push({ from_height: cursor, to_height: to });
|
|
236
|
+
}
|
|
237
|
+
return gaps;
|
|
238
|
+
}
|
|
239
|
+
function cursorHeight(cursor) {
|
|
240
|
+
if (!cursor)
|
|
241
|
+
return null;
|
|
242
|
+
try {
|
|
243
|
+
return decodeStreamsCursor(cursor).block_height;
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function topoSort(stages) {
|
|
249
|
+
const ids = new Set(stages.map((s) => s.id));
|
|
250
|
+
const incoming = new Map;
|
|
251
|
+
const outgoing = new Map;
|
|
252
|
+
for (const stage of stages) {
|
|
253
|
+
incoming.set(stage.id, 0);
|
|
254
|
+
outgoing.set(stage.id, []);
|
|
255
|
+
}
|
|
256
|
+
for (const stage of stages) {
|
|
257
|
+
if (!stage.depends_on || !ids.has(stage.depends_on))
|
|
258
|
+
continue;
|
|
259
|
+
outgoing.get(stage.depends_on)?.push(stage.id);
|
|
260
|
+
incoming.set(stage.id, (incoming.get(stage.id) ?? 0) + 1);
|
|
261
|
+
}
|
|
262
|
+
const queue = [...incoming.entries()].filter(([, n]) => n === 0).map(([id]) => id).sort();
|
|
263
|
+
const order = [];
|
|
264
|
+
while (queue.length > 0) {
|
|
265
|
+
const id = queue.shift();
|
|
266
|
+
if (!id)
|
|
267
|
+
break;
|
|
268
|
+
order.push(id);
|
|
269
|
+
for (const next of outgoing.get(id) ?? []) {
|
|
270
|
+
const left = (incoming.get(next) ?? 1) - 1;
|
|
271
|
+
incoming.set(next, left);
|
|
272
|
+
if (left === 0)
|
|
273
|
+
queue.push(next);
|
|
274
|
+
queue.sort();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const leftover = stages.map((s) => s.id).filter((id) => !order.includes(id));
|
|
278
|
+
return { order, cycles: leftover.length > 0 ? [leftover.sort()] : [] };
|
|
279
|
+
}
|
|
280
|
+
function lastByStage(rows) {
|
|
281
|
+
const map = new Map;
|
|
282
|
+
for (const row of rows)
|
|
283
|
+
map.set(row.stage_id, row);
|
|
284
|
+
return map;
|
|
285
|
+
}
|
|
286
|
+
function emptyEvidence(stageId) {
|
|
287
|
+
return {
|
|
288
|
+
stage_id: stageId,
|
|
289
|
+
ranges: [],
|
|
290
|
+
open_failures: [],
|
|
291
|
+
cursor: null,
|
|
292
|
+
queue: null,
|
|
293
|
+
unanchored: false,
|
|
294
|
+
unverified_import: false,
|
|
295
|
+
source_available: true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function declaredEnd(args) {
|
|
299
|
+
return args.run?.target_height ?? args.scope.target_height ?? args.source.finalized_height ?? args.source.tip_height;
|
|
300
|
+
}
|
|
301
|
+
function clockRanges(stage, evidence) {
|
|
302
|
+
if (stage.native_clock === "block") {
|
|
303
|
+
return { ranges: evidence.ranges, invalidCursor: false };
|
|
304
|
+
}
|
|
305
|
+
const cursor = stage.native_clock === "cursor" ? evidence.cursor : evidence.queue?.fence_cursor ?? null;
|
|
306
|
+
if (!cursor)
|
|
307
|
+
return { ranges: evidence.ranges, invalidCursor: false };
|
|
308
|
+
const height = cursorHeight(cursor);
|
|
309
|
+
if (height === null)
|
|
310
|
+
return { ranges: evidence.ranges, invalidCursor: true };
|
|
311
|
+
return {
|
|
312
|
+
ranges: mergeRanges([
|
|
313
|
+
...evidence.ranges,
|
|
314
|
+
{ from_height: 0, to_height: height }
|
|
315
|
+
]),
|
|
316
|
+
invalidCursor: false
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function queueCaught(queue) {
|
|
320
|
+
if (!queue)
|
|
321
|
+
return false;
|
|
322
|
+
return queue.accepted === queue.decided && queue.delivered + queue.dead === queue.accepted;
|
|
323
|
+
}
|
|
324
|
+
function sourceAgeSeconds(source, now) {
|
|
325
|
+
if (!source.observed_at)
|
|
326
|
+
return source.tip_height === null ? null : 0;
|
|
327
|
+
const observed = Date.parse(source.observed_at);
|
|
328
|
+
if (Number.isNaN(observed))
|
|
329
|
+
return null;
|
|
330
|
+
return Math.max(0, Math.round((now.getTime() - observed) / 1000));
|
|
331
|
+
}
|
|
332
|
+
function evaluateOne(stage, input, run, evidence, dep, missingDep, inCycle) {
|
|
333
|
+
const sourceTip = input.source.tip_height;
|
|
334
|
+
const finalized = input.source.finalized_height;
|
|
335
|
+
const recordedTarget = run?.target_height ?? input.scope.target_height;
|
|
336
|
+
const maxBehind = input.options.maxBlocksBehindFinalized ?? DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED;
|
|
337
|
+
const maxAge = input.options.maxSourceAgeSeconds ?? DEFAULT_MAX_SOURCE_AGE_SECONDS;
|
|
338
|
+
const age = sourceAgeSeconds(input.source, input.options.now);
|
|
339
|
+
const sourceFresh = sourceTip !== null && finalized !== null && (age === null || age <= maxAge);
|
|
340
|
+
const base = {
|
|
341
|
+
stage_id: stage.id,
|
|
342
|
+
kind: stage.kind,
|
|
343
|
+
native_clock: stage.native_clock,
|
|
344
|
+
depends_on: stage.depends_on,
|
|
345
|
+
source_tip: sourceTip,
|
|
346
|
+
finalized_height: finalized,
|
|
347
|
+
recorded_target: recordedTarget
|
|
348
|
+
};
|
|
349
|
+
const done = (state, detail, extra = {}) => {
|
|
350
|
+
const completeThrough2 = extra.complete_through ?? null;
|
|
351
|
+
const caughtUp = extra.caught_up ?? (state !== "disabled" && state !== "failed" && state !== "gap" && state !== "out_of_scope" && state !== "unverified_import" && state !== "unanchored" && state !== "source_unavailable" && completeThrough2 !== null && finalized !== null && completeThrough2 >= finalized && sourceFresh);
|
|
352
|
+
return {
|
|
353
|
+
...base,
|
|
354
|
+
state,
|
|
355
|
+
detail,
|
|
356
|
+
complete_through: completeThrough2,
|
|
357
|
+
declared_range: extra.declared_range ?? null,
|
|
358
|
+
gaps: extra.gaps ?? [],
|
|
359
|
+
caught_up: caughtUp,
|
|
360
|
+
blocked_by: extra.blocked_by ?? null
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
if (!stage.enabled) {
|
|
364
|
+
return done("disabled", "stage is disabled");
|
|
365
|
+
}
|
|
366
|
+
if (inCycle) {
|
|
367
|
+
return done("failed", "dependency cycle");
|
|
368
|
+
}
|
|
369
|
+
if (missingDep) {
|
|
370
|
+
return done("failed", `depends on unknown stage ${stage.depends_on}`);
|
|
371
|
+
}
|
|
372
|
+
if (dep && HARD_DEP_STATES.has(dep.state)) {
|
|
373
|
+
return done(dep.state, `blocked by ${dep.stage_id}: ${dep.detail}`, {
|
|
374
|
+
complete_through: dep.complete_through,
|
|
375
|
+
blocked_by: dep.stage_id,
|
|
376
|
+
declared_range: dep.declared_range,
|
|
377
|
+
gaps: dep.gaps,
|
|
378
|
+
caught_up: false
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
if (evidence.open_failures.length > 0 || run?.status === "failed") {
|
|
382
|
+
const first = evidence.open_failures[0];
|
|
383
|
+
return done("failed", first ? `open ${first.class} failure (${first.retry_state})` : "run marked failed");
|
|
384
|
+
}
|
|
385
|
+
if (evidence.unverified_import || run?.status === "unverified_import") {
|
|
386
|
+
return done("unverified_import", "imported data has not been audited");
|
|
387
|
+
}
|
|
388
|
+
if (evidence.unanchored || run?.status === "unanchored") {
|
|
389
|
+
return done("unanchored", "receipts are missing a block hash or node anchor");
|
|
390
|
+
}
|
|
391
|
+
if (!evidence.source_available) {
|
|
392
|
+
return done("source_unavailable", "native source is unavailable");
|
|
393
|
+
}
|
|
394
|
+
const { ranges, invalidCursor } = clockRanges(stage, evidence);
|
|
395
|
+
if (invalidCursor) {
|
|
396
|
+
return done("failed", "native clock cursor is not a valid streams cursor");
|
|
397
|
+
}
|
|
398
|
+
const end = declaredEnd({ run, scope: input.scope, source: input.source });
|
|
399
|
+
if (end !== null && end < input.scope.start_height) {
|
|
400
|
+
return done("out_of_scope", "recorded target is below the instance start");
|
|
401
|
+
}
|
|
402
|
+
const sourceSilent = sourceTip === null && finalized === null && recordedTarget === null;
|
|
403
|
+
if (sourceSilent) {
|
|
404
|
+
return done("source_unavailable", "the chain source could not be reached, so the declared range is unknown");
|
|
405
|
+
}
|
|
406
|
+
if (end === null) {
|
|
407
|
+
return done("source_unavailable", "no recorded target and the source tip is unknown");
|
|
408
|
+
}
|
|
409
|
+
const declaredRange = {
|
|
410
|
+
from_height: input.scope.start_height,
|
|
411
|
+
to_height: end
|
|
412
|
+
};
|
|
413
|
+
let completeThrough = contiguousThrough(ranges, input.scope.start_height);
|
|
414
|
+
if (dep?.complete_through !== null && dep?.complete_through !== undefined) {
|
|
415
|
+
if (completeThrough === null) {} else {
|
|
416
|
+
completeThrough = Math.min(completeThrough, dep.complete_through);
|
|
417
|
+
}
|
|
418
|
+
} else if (dep && dep.complete_through === null) {
|
|
419
|
+
completeThrough = null;
|
|
420
|
+
}
|
|
421
|
+
const gaps = findRangeGaps(ranges, declaredRange.from_height, declaredRange.to_height);
|
|
422
|
+
const tiled = gaps.length === 0 && completeThrough !== null && completeThrough >= declaredRange.to_height;
|
|
423
|
+
const trailingGap = gaps.length === 1 && gaps[0].to_height === declaredRange.to_height && (completeThrough === null ? gaps[0].from_height === declaredRange.from_height : gaps[0].from_height === completeThrough + 1);
|
|
424
|
+
const queueIncomplete = stage.native_clock === "queue" && !queueCaught(evidence.queue);
|
|
425
|
+
if (gaps.length > 0 && !trailingGap) {
|
|
426
|
+
const first = gaps[0];
|
|
427
|
+
return done("gap", `missing ${first.from_height}–${first.to_height}`, {
|
|
428
|
+
complete_through: completeThrough,
|
|
429
|
+
declared_range: declaredRange,
|
|
430
|
+
gaps,
|
|
431
|
+
caught_up: false
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (queueIncomplete || !tiled) {
|
|
435
|
+
return done("syncing", "contiguous from start, not yet through the recorded target", {
|
|
436
|
+
complete_through: completeThrough,
|
|
437
|
+
declared_range: declaredRange,
|
|
438
|
+
gaps,
|
|
439
|
+
caught_up: false
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const sourceStale = age !== null && age > maxAge;
|
|
443
|
+
const behindFinalized = finalized !== null && completeThrough !== null ? Math.max(0, finalized - completeThrough) : null;
|
|
444
|
+
const behindTip = sourceTip !== null && completeThrough !== null ? Math.max(0, sourceTip - completeThrough) : null;
|
|
445
|
+
const pinned = recordedTarget !== null && finalized !== null && recordedTarget < finalized;
|
|
446
|
+
if (sourceStale) {
|
|
447
|
+
return done("stale", `source observation is ${age}s old, beyond the ${maxAge}s freshness objective`, {
|
|
448
|
+
complete_through: completeThrough,
|
|
449
|
+
declared_range: declaredRange,
|
|
450
|
+
gaps,
|
|
451
|
+
caught_up: false
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (behindFinalized !== null && behindFinalized > maxBehind) {
|
|
455
|
+
return done("stale", `${behindFinalized} blocks behind the finalized height, beyond the ${maxBehind} threshold`, {
|
|
456
|
+
complete_through: completeThrough,
|
|
457
|
+
declared_range: declaredRange,
|
|
458
|
+
gaps,
|
|
459
|
+
caught_up: false
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
if (!pinned && behindFinalized !== null && behindFinalized > 0) {
|
|
463
|
+
return done("lagging", `${behindFinalized} blocks behind the finalized height`, {
|
|
464
|
+
complete_through: completeThrough,
|
|
465
|
+
declared_range: declaredRange,
|
|
466
|
+
gaps,
|
|
467
|
+
caught_up: false
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (!pinned && behindTip !== null && behindTip > 0) {
|
|
471
|
+
return done("lagging", `${behindTip} blocks behind the chain tip`, {
|
|
472
|
+
complete_through: completeThrough,
|
|
473
|
+
declared_range: declaredRange,
|
|
474
|
+
gaps
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return done("complete", "processed every unit through the recorded target", {
|
|
478
|
+
complete_through: completeThrough,
|
|
479
|
+
declared_range: declaredRange,
|
|
480
|
+
gaps
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function evaluateCoverage(input) {
|
|
484
|
+
const { order, cycles } = topoSort(input.stages);
|
|
485
|
+
const cyclic = new Set(cycles.flat());
|
|
486
|
+
const known = new Set(input.stages.map((s) => s.id));
|
|
487
|
+
const runs = lastByStage(input.runs);
|
|
488
|
+
const evidence = lastByStage(input.evidence);
|
|
489
|
+
const byId = new Map(input.stages.map((s) => [s.id, s]));
|
|
490
|
+
const evaluated = new Map;
|
|
491
|
+
const visit = (id) => {
|
|
492
|
+
const stage = byId.get(id);
|
|
493
|
+
if (!stage || evaluated.has(id))
|
|
494
|
+
return;
|
|
495
|
+
const depId = stage.depends_on;
|
|
496
|
+
const missingDep = Boolean(depId && !known.has(depId));
|
|
497
|
+
const dep = depId && !missingDep ? evaluated.get(depId) ?? null : null;
|
|
498
|
+
evaluated.set(id, evaluateOne(stage, input, runs.get(id), evidence.get(id) ?? emptyEvidence(id), dep, missingDep, cyclic.has(id)));
|
|
499
|
+
};
|
|
500
|
+
for (const id of order)
|
|
501
|
+
visit(id);
|
|
502
|
+
for (const id of cyclic)
|
|
503
|
+
visit(id);
|
|
504
|
+
const stages = [...evaluated.values()].sort((a, b) => a.stage_id.localeCompare(b.stage_id));
|
|
505
|
+
return {
|
|
506
|
+
schema_version: COVERAGE_REPORT_SCHEMA_VERSION,
|
|
507
|
+
network: input.scope.network,
|
|
508
|
+
generated_at: input.options.now.toISOString(),
|
|
509
|
+
source_tip: input.source.tip_height,
|
|
510
|
+
finalized_height: input.source.finalized_height,
|
|
511
|
+
stages,
|
|
512
|
+
evaluation_order: order,
|
|
513
|
+
cycles
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
// src/coverage/runner.ts
|
|
517
|
+
var RUNNER_STATUSES = [
|
|
518
|
+
"pending",
|
|
519
|
+
"running",
|
|
520
|
+
"complete",
|
|
521
|
+
"failed",
|
|
522
|
+
"halted"
|
|
523
|
+
];
|
|
524
|
+
var RUNNER_EVENT_TYPES = [
|
|
525
|
+
"start",
|
|
526
|
+
"ack",
|
|
527
|
+
"fail",
|
|
528
|
+
"retry",
|
|
529
|
+
"halt",
|
|
530
|
+
"resume",
|
|
531
|
+
"reorg",
|
|
532
|
+
"version",
|
|
533
|
+
"finish"
|
|
534
|
+
];
|
|
535
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
536
|
+
var RUNNER_TRANSITION_TABLE = [
|
|
537
|
+
{ from: "pending", event: "start", to: "running" },
|
|
538
|
+
{ from: "pending", event: "ack", to: "reject" },
|
|
539
|
+
{ from: "pending", event: "fail", to: "reject" },
|
|
540
|
+
{ from: "pending", event: "retry", to: "reject" },
|
|
541
|
+
{ from: "pending", event: "halt", to: "reject" },
|
|
542
|
+
{ from: "pending", event: "resume", to: "reject" },
|
|
543
|
+
{ from: "pending", event: "reorg", to: "reject" },
|
|
544
|
+
{ from: "pending", event: "version", to: "pending" },
|
|
545
|
+
{ from: "pending", event: "finish", to: "reject" },
|
|
546
|
+
{ from: "running", event: "start", to: "reject" },
|
|
547
|
+
{ from: "running", event: "ack", to: "running" },
|
|
548
|
+
{ from: "running", event: "fail", to: "failed" },
|
|
549
|
+
{ from: "running", event: "retry", to: "reject" },
|
|
550
|
+
{ from: "running", event: "halt", to: "halted" },
|
|
551
|
+
{ from: "running", event: "resume", to: "running" },
|
|
552
|
+
{ from: "running", event: "reorg", to: "running" },
|
|
553
|
+
{ from: "running", event: "version", to: "halted" },
|
|
554
|
+
{ from: "running", event: "finish", to: "complete" },
|
|
555
|
+
{ from: "complete", event: "start", to: "reject" },
|
|
556
|
+
{ from: "complete", event: "ack", to: "reject" },
|
|
557
|
+
{ from: "complete", event: "fail", to: "reject" },
|
|
558
|
+
{ from: "complete", event: "retry", to: "reject" },
|
|
559
|
+
{ from: "complete", event: "halt", to: "reject" },
|
|
560
|
+
{ from: "complete", event: "resume", to: "reject" },
|
|
561
|
+
{ from: "complete", event: "reorg", to: "running" },
|
|
562
|
+
{ from: "complete", event: "version", to: "complete" },
|
|
563
|
+
{ from: "complete", event: "finish", to: "complete" },
|
|
564
|
+
{ from: "failed", event: "start", to: "reject" },
|
|
565
|
+
{ from: "failed", event: "ack", to: "reject" },
|
|
566
|
+
{ from: "failed", event: "fail", to: "reject" },
|
|
567
|
+
{ from: "failed", event: "retry", to: "running" },
|
|
568
|
+
{ from: "failed", event: "halt", to: "halted" },
|
|
569
|
+
{ from: "failed", event: "resume", to: "running" },
|
|
570
|
+
{ from: "failed", event: "reorg", to: "failed" },
|
|
571
|
+
{ from: "failed", event: "version", to: "halted" },
|
|
572
|
+
{ from: "failed", event: "finish", to: "reject" },
|
|
573
|
+
{ from: "halted", event: "start", to: "reject" },
|
|
574
|
+
{ from: "halted", event: "ack", to: "reject" },
|
|
575
|
+
{ from: "halted", event: "fail", to: "reject" },
|
|
576
|
+
{ from: "halted", event: "retry", to: "running" },
|
|
577
|
+
{ from: "halted", event: "halt", to: "halted" },
|
|
578
|
+
{ from: "halted", event: "resume", to: "running" },
|
|
579
|
+
{ from: "halted", event: "reorg", to: "halted" },
|
|
580
|
+
{ from: "halted", event: "version", to: "halted" },
|
|
581
|
+
{ from: "halted", event: "finish", to: "reject" }
|
|
582
|
+
];
|
|
583
|
+
var RULE_INDEX = new Map(RUNNER_TRANSITION_TABLE.map((rule) => [`${rule.from}:${rule.event}`, rule]));
|
|
584
|
+
function lookupTransition(from, event) {
|
|
585
|
+
const rule = RULE_INDEX.get(`${from}:${event}`);
|
|
586
|
+
if (!rule) {
|
|
587
|
+
throw new Error(`missing transition ${from} + ${event}`);
|
|
588
|
+
}
|
|
589
|
+
return rule;
|
|
590
|
+
}
|
|
591
|
+
function versionsEqual(a, b) {
|
|
592
|
+
return a.code_hash === b.code_hash && a.config_hash === b.config_hash && a.handler_hash === b.handler_hash;
|
|
593
|
+
}
|
|
594
|
+
function expectedAckHeight(state) {
|
|
595
|
+
return state.complete_through === null ? state.start_height : state.complete_through + 1;
|
|
596
|
+
}
|
|
597
|
+
function createRunnerState(init) {
|
|
598
|
+
return {
|
|
599
|
+
stage_id: init.stage_id,
|
|
600
|
+
status: "pending",
|
|
601
|
+
version: init.version,
|
|
602
|
+
start_height: init.start_height,
|
|
603
|
+
target_height: init.target_height ?? null,
|
|
604
|
+
complete_through: null,
|
|
605
|
+
last_acked_hash: null,
|
|
606
|
+
retry_count: 0,
|
|
607
|
+
open_failure: null
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function ok(state, effects = []) {
|
|
611
|
+
return { ok: true, state, effects };
|
|
612
|
+
}
|
|
613
|
+
function reject(state, reason) {
|
|
614
|
+
return { ok: false, reason, state };
|
|
615
|
+
}
|
|
616
|
+
function clone(state, patch) {
|
|
617
|
+
return { ...state, ...patch };
|
|
618
|
+
}
|
|
619
|
+
function rewind(state, forkPoint) {
|
|
620
|
+
const from = Math.max(forkPoint, state.start_height);
|
|
621
|
+
const through = state.complete_through;
|
|
622
|
+
if (through === null || through < from) {
|
|
623
|
+
return { next: state, effects: [] };
|
|
624
|
+
}
|
|
625
|
+
const completeThrough = from <= state.start_height ? null : from - 1;
|
|
626
|
+
return {
|
|
627
|
+
next: clone(state, {
|
|
628
|
+
complete_through: completeThrough,
|
|
629
|
+
last_acked_hash: null
|
|
630
|
+
}),
|
|
631
|
+
effects: [
|
|
632
|
+
{
|
|
633
|
+
type: "invalidate_receipts",
|
|
634
|
+
range: { from_height: from, to_height: through }
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function applyRetry(state, maxRetries) {
|
|
640
|
+
if (state.retry_count >= maxRetries) {
|
|
641
|
+
const failure2 = state.open_failure ? { ...state.open_failure, retry_state: "halted" } : null;
|
|
642
|
+
return ok(clone(state, { status: "halted", open_failure: failure2 }));
|
|
643
|
+
}
|
|
644
|
+
const failure = state.open_failure ? { ...state.open_failure, retry_state: "retrying" } : null;
|
|
645
|
+
return ok(clone(state, {
|
|
646
|
+
status: "running",
|
|
647
|
+
retry_count: state.retry_count + 1,
|
|
648
|
+
open_failure: failure
|
|
649
|
+
}));
|
|
650
|
+
}
|
|
651
|
+
function applyRunnerEvent(state, event, opts) {
|
|
652
|
+
const maxRetries = opts?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
653
|
+
const rule = lookupTransition(state.status, event.type);
|
|
654
|
+
if (rule.to === "reject") {
|
|
655
|
+
return reject(state, `${state.status} cannot ${event.type}`);
|
|
656
|
+
}
|
|
657
|
+
const nextStatus = rule.to;
|
|
658
|
+
switch (event.type) {
|
|
659
|
+
case "start":
|
|
660
|
+
return ok(clone(state, { status: "running" }));
|
|
661
|
+
case "ack": {
|
|
662
|
+
if (event.height === state.complete_through) {
|
|
663
|
+
if (event.hash === state.last_acked_hash)
|
|
664
|
+
return ok(state);
|
|
665
|
+
return reject(state, `ack at ${event.height} has a different hash; reorg first`);
|
|
666
|
+
}
|
|
667
|
+
const expected = expectedAckHeight(state);
|
|
668
|
+
if (event.height !== expected) {
|
|
669
|
+
return reject(state, `ack at ${event.height} skips ${expected}; acknowledgements must be ordered`);
|
|
670
|
+
}
|
|
671
|
+
const effects = [];
|
|
672
|
+
const failure = state.open_failure;
|
|
673
|
+
const coversFailure = failure && failure.from_height !== null && event.height >= failure.from_height && (failure.to_height === null || event.height <= failure.to_height);
|
|
674
|
+
if (coversFailure)
|
|
675
|
+
effects.push({ type: "resolve_failure" });
|
|
676
|
+
return ok(clone(state, {
|
|
677
|
+
status: "running",
|
|
678
|
+
complete_through: event.height,
|
|
679
|
+
last_acked_hash: event.hash,
|
|
680
|
+
open_failure: coversFailure ? null : failure
|
|
681
|
+
}), effects);
|
|
682
|
+
}
|
|
683
|
+
case "fail": {
|
|
684
|
+
const failure = {
|
|
685
|
+
unit_kind: event.unit_kind,
|
|
686
|
+
class: event.class,
|
|
687
|
+
retry_state: "open",
|
|
688
|
+
from_height: event.from_height,
|
|
689
|
+
to_height: event.to_height,
|
|
690
|
+
error: event.error
|
|
691
|
+
};
|
|
692
|
+
return ok(clone(state, { status: "failed", open_failure: failure }), [
|
|
693
|
+
{ type: "open_failure", failure }
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
case "retry":
|
|
697
|
+
return applyRetry(state, maxRetries);
|
|
698
|
+
case "halt": {
|
|
699
|
+
const failure = state.open_failure ? { ...state.open_failure, retry_state: "halted" } : state.open_failure;
|
|
700
|
+
return ok(clone(state, { status: "halted", open_failure: failure }));
|
|
701
|
+
}
|
|
702
|
+
case "resume":
|
|
703
|
+
if (state.status === "running")
|
|
704
|
+
return ok(state);
|
|
705
|
+
if (state.open_failure)
|
|
706
|
+
return applyRetry(state, maxRetries);
|
|
707
|
+
return ok(clone(state, { status: "running" }));
|
|
708
|
+
case "reorg": {
|
|
709
|
+
if (event.fork_point < state.start_height) {
|
|
710
|
+
return reject(state, `reorg fork ${event.fork_point} is below start ${state.start_height}`);
|
|
711
|
+
}
|
|
712
|
+
const { next, effects } = rewind(state, event.fork_point);
|
|
713
|
+
if (effects.length === 0)
|
|
714
|
+
return ok(state);
|
|
715
|
+
return ok(clone(next, { status: nextStatus }), effects);
|
|
716
|
+
}
|
|
717
|
+
case "version": {
|
|
718
|
+
if (versionsEqual(state.version, event.version))
|
|
719
|
+
return ok(state);
|
|
720
|
+
if (state.status === "pending") {
|
|
721
|
+
return ok(clone(state, { version: event.version }));
|
|
722
|
+
}
|
|
723
|
+
const sealed = state.status === "complete" ? "complete" : "halted";
|
|
724
|
+
return ok(clone(state, { status: sealed }), [
|
|
725
|
+
{ type: "spawn_run", version: event.version }
|
|
726
|
+
]);
|
|
727
|
+
}
|
|
728
|
+
case "finish": {
|
|
729
|
+
if (state.status === "complete")
|
|
730
|
+
return ok(state);
|
|
731
|
+
if (state.target_height === null) {
|
|
732
|
+
return reject(state, "cannot finish a follow-the-chain run");
|
|
733
|
+
}
|
|
734
|
+
if (state.complete_through === null || state.complete_through < state.target_height) {
|
|
735
|
+
return reject(state, `cannot finish: complete_through ${state.complete_through} is short of target ${state.target_height}`);
|
|
736
|
+
}
|
|
737
|
+
return ok(clone(state, { status: "complete" }));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// src/coverage/decoder-clock.ts
|
|
742
|
+
import { createHash } from "node:crypto";
|
|
743
|
+
function inputDigest(cursors) {
|
|
744
|
+
if (cursors.length === 0) {
|
|
745
|
+
return createHash("sha256").update(`no-match
|
|
746
|
+
`).digest("hex");
|
|
747
|
+
}
|
|
748
|
+
const hash = createHash("sha256");
|
|
749
|
+
for (const cursor of cursors)
|
|
750
|
+
hash.update(`${cursor}
|
|
751
|
+
`);
|
|
752
|
+
return hash.digest("hex");
|
|
753
|
+
}
|
|
754
|
+
function cursorIsAfterBlock(cursor, height) {
|
|
755
|
+
return compareStreamsCursor(cursor, blockEndCursor(height)) >= 0;
|
|
756
|
+
}
|
|
757
|
+
function cursorIsBeforeBlock(cursor, height) {
|
|
758
|
+
return cursor.block_height < height;
|
|
759
|
+
}
|
|
760
|
+
function parseCursor(value, label) {
|
|
761
|
+
try {
|
|
762
|
+
return { ok: true, cursor: decodeStreamsCursor(value) };
|
|
763
|
+
} catch {
|
|
764
|
+
return { ok: false, reason: `${label} is not a valid streams cursor` };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function planDecoderReceipts(input) {
|
|
768
|
+
const to = parseCursor(input.to_cursor, "to_cursor");
|
|
769
|
+
if (!to.ok)
|
|
770
|
+
return { ok: false, reason: to.reason, receipts: [] };
|
|
771
|
+
let from = null;
|
|
772
|
+
if (input.from_cursor !== null) {
|
|
773
|
+
const parsed = parseCursor(input.from_cursor, "from_cursor");
|
|
774
|
+
if (!parsed.ok)
|
|
775
|
+
return { ok: false, reason: parsed.reason, receipts: [] };
|
|
776
|
+
from = parsed.cursor;
|
|
777
|
+
if (compareStreamsCursor(from, to.cursor) >= 0) {
|
|
778
|
+
return {
|
|
779
|
+
ok: false,
|
|
780
|
+
reason: "to_cursor must be after from_cursor",
|
|
781
|
+
receipts: []
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const startFull = from === null ? input.start_height : from.block_height + 1;
|
|
786
|
+
const lastFull = isEmptyRangeCursor(to.cursor) ? to.cursor.block_height : to.cursor.block_height - 1;
|
|
787
|
+
if (lastFull < startFull) {
|
|
788
|
+
return { ok: true, receipts: [], through_cursor: input.to_cursor };
|
|
789
|
+
}
|
|
790
|
+
if (startFull < input.start_height) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
reason: `window starts at ${startFull}, below start_height ${input.start_height}`,
|
|
794
|
+
receipts: []
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const byHeight = new Map;
|
|
798
|
+
let prevHeight = null;
|
|
799
|
+
for (const block of input.blocks) {
|
|
800
|
+
if (prevHeight !== null && block.height !== prevHeight + 1) {
|
|
801
|
+
return {
|
|
802
|
+
ok: false,
|
|
803
|
+
reason: `blocks are not contiguous (${prevHeight} → ${block.height})`,
|
|
804
|
+
receipts: []
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
if (byHeight.has(block.height)) {
|
|
808
|
+
return {
|
|
809
|
+
ok: false,
|
|
810
|
+
reason: `duplicate canonical height ${block.height}`,
|
|
811
|
+
receipts: []
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
byHeight.set(block.height, block);
|
|
815
|
+
prevHeight = block.height;
|
|
816
|
+
}
|
|
817
|
+
for (let height = startFull;height <= lastFull; height++) {
|
|
818
|
+
if (!byHeight.has(height)) {
|
|
819
|
+
return {
|
|
820
|
+
ok: false,
|
|
821
|
+
reason: `missing canonical block ${height}`,
|
|
822
|
+
receipts: []
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const decoded = [];
|
|
827
|
+
let previous = from;
|
|
828
|
+
for (const event of input.events) {
|
|
829
|
+
const parsed = parseCursor(event.cursor, "event.cursor");
|
|
830
|
+
if (!parsed.ok)
|
|
831
|
+
return { ok: false, reason: parsed.reason, receipts: [] };
|
|
832
|
+
if (previous && compareStreamsCursor(parsed.cursor, previous) <= 0) {
|
|
833
|
+
return {
|
|
834
|
+
ok: false,
|
|
835
|
+
reason: `event cursor ${event.cursor} is not after ${encodeStreamsCursor(previous)}`,
|
|
836
|
+
receipts: []
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
if (compareStreamsCursor(parsed.cursor, to.cursor) >= 0) {
|
|
840
|
+
return {
|
|
841
|
+
ok: false,
|
|
842
|
+
reason: `event cursor ${event.cursor} is not before to_cursor`,
|
|
843
|
+
receipts: []
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
if (from && compareStreamsCursor(parsed.cursor, from) <= 0) {
|
|
847
|
+
return {
|
|
848
|
+
ok: false,
|
|
849
|
+
reason: `event cursor ${event.cursor} is not after from_cursor`,
|
|
850
|
+
receipts: []
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const block = byHeight.get(parsed.cursor.block_height);
|
|
854
|
+
if (!block) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
reason: `event ${event.cursor} has no canonical block`,
|
|
858
|
+
receipts: []
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
if (block.hash !== event.block_hash) {
|
|
862
|
+
return {
|
|
863
|
+
ok: false,
|
|
864
|
+
reason: `event ${event.cursor} hash ${event.block_hash} does not match canonical ${block.hash}`,
|
|
865
|
+
receipts: []
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
decoded.push({
|
|
869
|
+
cursor: parsed.cursor,
|
|
870
|
+
encoded: event.cursor,
|
|
871
|
+
block_hash: event.block_hash,
|
|
872
|
+
matched: event.matched
|
|
873
|
+
});
|
|
874
|
+
previous = parsed.cursor;
|
|
875
|
+
}
|
|
876
|
+
const receipts = [];
|
|
877
|
+
for (let height = startFull;height <= lastFull; height++) {
|
|
878
|
+
const block = byHeight.get(height);
|
|
879
|
+
if (!block) {
|
|
880
|
+
return {
|
|
881
|
+
ok: false,
|
|
882
|
+
reason: `missing canonical block ${height}`,
|
|
883
|
+
receipts
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const inBlock = decoded.filter((e) => e.cursor.block_height === height);
|
|
887
|
+
const matched = inBlock.filter((e) => e.matched);
|
|
888
|
+
const last = inBlock[inBlock.length - 1];
|
|
889
|
+
const through = last ? last.encoded : encodeStreamsCursor(blockEndCursor(height));
|
|
890
|
+
receipts.push({
|
|
891
|
+
height,
|
|
892
|
+
hash: block.hash,
|
|
893
|
+
input_count: matched.length,
|
|
894
|
+
input_cursors: matched.map((e) => e.encoded),
|
|
895
|
+
input_digest: inputDigest(matched.map((e) => e.encoded)),
|
|
896
|
+
through_cursor: through,
|
|
897
|
+
no_match: matched.length === 0
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
return { ok: true, receipts, through_cursor: input.to_cursor };
|
|
901
|
+
}
|
|
902
|
+
// src/coverage/adapter.ts
|
|
903
|
+
var DECODER_COMMIT_STEPS = [
|
|
904
|
+
"output",
|
|
905
|
+
"checkpoint",
|
|
906
|
+
"receipt",
|
|
907
|
+
"failure"
|
|
908
|
+
];
|
|
909
|
+
|
|
910
|
+
class DecoderAdapterCrash extends Error {
|
|
911
|
+
step;
|
|
912
|
+
name = "DecoderAdapterCrash";
|
|
913
|
+
constructor(step) {
|
|
914
|
+
super(`decoder adapter crash after ${step}`);
|
|
915
|
+
this.step = step;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function runDecoderCommitSteps(steps, crashAfter) {
|
|
919
|
+
for (const step of DECODER_COMMIT_STEPS) {
|
|
920
|
+
await steps[step]();
|
|
921
|
+
if (crashAfter === step)
|
|
922
|
+
throw new DecoderAdapterCrash(step);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function applyDecoderReceipts(state, receipts) {
|
|
926
|
+
let current = state;
|
|
927
|
+
if (current.status === "pending") {
|
|
928
|
+
const started = applyRunnerEvent(current, { type: "start" });
|
|
929
|
+
if (!started.ok)
|
|
930
|
+
return started;
|
|
931
|
+
current = started.state;
|
|
932
|
+
}
|
|
933
|
+
for (const receipt of receipts) {
|
|
934
|
+
const event = {
|
|
935
|
+
type: "ack",
|
|
936
|
+
height: receipt.height,
|
|
937
|
+
hash: receipt.hash,
|
|
938
|
+
input_count: receipt.input_count,
|
|
939
|
+
input_digest: receipt.input_digest,
|
|
940
|
+
effect_digest: receipt.effect_digest
|
|
941
|
+
};
|
|
942
|
+
const next = applyRunnerEvent(current, event);
|
|
943
|
+
if (!next.ok)
|
|
944
|
+
return next;
|
|
945
|
+
current = next.state;
|
|
946
|
+
}
|
|
947
|
+
return { ok: true, state: current, effects: [] };
|
|
948
|
+
}
|
|
949
|
+
async function upsertRegistry(tx, stageId) {
|
|
950
|
+
await tx.insertInto("stage_registry").values({
|
|
951
|
+
id: stageId,
|
|
952
|
+
kind: "decode",
|
|
953
|
+
depends_on: null,
|
|
954
|
+
native_clock: "block",
|
|
955
|
+
producer_version: "v1",
|
|
956
|
+
repair_mode: "full_reindex"
|
|
957
|
+
}).onConflict((oc) => oc.column("id").doNothing()).execute();
|
|
958
|
+
}
|
|
959
|
+
async function writeCheckpoint(tx, decoderName, cursor) {
|
|
960
|
+
if (cursor === null)
|
|
961
|
+
return;
|
|
962
|
+
await tx.insertInto("decoder_checkpoints").values({
|
|
963
|
+
decoder_name: decoderName,
|
|
964
|
+
last_cursor: cursor
|
|
965
|
+
}).onConflict((oc) => oc.column("decoder_name").doUpdateSet({
|
|
966
|
+
last_cursor: cursor,
|
|
967
|
+
updated_at: new Date
|
|
968
|
+
})).execute();
|
|
969
|
+
}
|
|
970
|
+
async function writeReceipts(tx, stageId, runId, receipts) {
|
|
971
|
+
if (receipts.length === 0)
|
|
972
|
+
return;
|
|
973
|
+
await tx.insertInto("stage_block_receipts").values(receipts.map((receipt) => ({
|
|
974
|
+
stage_id: stageId,
|
|
975
|
+
run_id: runId,
|
|
976
|
+
block_height: receipt.height,
|
|
977
|
+
block_hash: receipt.hash,
|
|
978
|
+
input_count: receipt.input_count,
|
|
979
|
+
input_digest: receipt.input_digest,
|
|
980
|
+
effect_digest: receipt.effect_digest
|
|
981
|
+
}))).onConflict((oc) => oc.columns(["stage_id", "block_height", "block_hash"]).doUpdateSet((eb) => ({
|
|
982
|
+
input_count: eb.ref("excluded.input_count"),
|
|
983
|
+
input_digest: eb.ref("excluded.input_digest"),
|
|
984
|
+
effect_digest: eb.ref("excluded.effect_digest"),
|
|
985
|
+
run_id: eb.ref("excluded.run_id")
|
|
986
|
+
}))).execute();
|
|
987
|
+
}
|
|
988
|
+
async function writeFailure(tx, stageId, runId, failure) {
|
|
989
|
+
await tx.insertInto("stage_failures").values({
|
|
990
|
+
stage_id: stageId,
|
|
991
|
+
run_id: runId,
|
|
992
|
+
unit_kind: failure.unit_kind,
|
|
993
|
+
from_height: failure.from_height,
|
|
994
|
+
to_height: failure.to_height,
|
|
995
|
+
class: failure.class,
|
|
996
|
+
retry_state: failure.retry_state,
|
|
997
|
+
retry_count: failure.retry_count ?? 0,
|
|
998
|
+
last_error: failure.error
|
|
999
|
+
}).execute();
|
|
1000
|
+
}
|
|
1001
|
+
async function commitDecoderAdapter(db, input) {
|
|
1002
|
+
const runId = input.run_id ?? null;
|
|
1003
|
+
const failure = input.failure ?? null;
|
|
1004
|
+
await db.transaction().execute(async (tx) => {
|
|
1005
|
+
await upsertRegistry(tx, input.stage_id);
|
|
1006
|
+
await runDecoderCommitSteps({
|
|
1007
|
+
output: () => input.writeOutput(tx),
|
|
1008
|
+
checkpoint: () => writeCheckpoint(tx, input.decoder_name, input.checkpoint_cursor),
|
|
1009
|
+
receipt: () => writeReceipts(tx, input.stage_id, runId, input.receipts),
|
|
1010
|
+
failure: () => failure ? writeFailure(tx, input.stage_id, runId, failure) : Promise.resolve()
|
|
1011
|
+
}, input.crashAfter);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
// src/coverage/sealer.ts
|
|
1015
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1016
|
+
function rangeDigestOf(parts) {
|
|
1017
|
+
const hash = createHash2("sha256");
|
|
1018
|
+
for (const part of parts)
|
|
1019
|
+
hash.update(`${part}
|
|
1020
|
+
`);
|
|
1021
|
+
return hash.digest("hex");
|
|
1022
|
+
}
|
|
1023
|
+
function sealFinalizedRange(receipts, range) {
|
|
1024
|
+
const inRange = receipts.filter((r) => r.height >= range.from_height && r.height <= range.to_height).sort((a, b) => a.height - b.height);
|
|
1025
|
+
if (inRange.length === 0) {
|
|
1026
|
+
return { ok: false, reason: "no receipts in range" };
|
|
1027
|
+
}
|
|
1028
|
+
let expected = range.from_height;
|
|
1029
|
+
for (const receipt of inRange) {
|
|
1030
|
+
if (!receipt.finalized) {
|
|
1031
|
+
return { ok: false, reason: `unfinalized receipt at ${receipt.height}` };
|
|
1032
|
+
}
|
|
1033
|
+
if (receipt.height !== expected) {
|
|
1034
|
+
return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
reason: `gap at ${expected} before ${receipt.height}`
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
expected += 1;
|
|
1040
|
+
}
|
|
1041
|
+
if (inRange[inRange.length - 1]?.height !== range.to_height) {
|
|
1042
|
+
return { ok: false, reason: "range is not fully covered" };
|
|
1043
|
+
}
|
|
1044
|
+
return {
|
|
1045
|
+
ok: true,
|
|
1046
|
+
compacted: inRange.length,
|
|
1047
|
+
segment: {
|
|
1048
|
+
from_height: range.from_height,
|
|
1049
|
+
to_height: range.to_height,
|
|
1050
|
+
chain_digest: rangeDigestOf(inRange.map((r) => `${r.height}:${r.hash}`)),
|
|
1051
|
+
input_digest: rangeDigestOf(inRange.map((r) => r.input_digest)),
|
|
1052
|
+
output_digest: rangeDigestOf(inRange.map((r) => r.effect_digest))
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function segmentsSurviveReorg(segments, fork) {
|
|
1057
|
+
return segments.filter((s) => s.to_height < fork);
|
|
1058
|
+
}
|
|
1059
|
+
function receiptsAfterReorg(receipts, fork) {
|
|
1060
|
+
return receipts.filter((r) => r.height < fork);
|
|
1061
|
+
}
|
|
1062
|
+
// src/coverage/effect-manifest.ts
|
|
1063
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1064
|
+
function canonicalizeMutations(ops) {
|
|
1065
|
+
return ops.map((op) => ({
|
|
1066
|
+
op: op.op,
|
|
1067
|
+
table: op.table,
|
|
1068
|
+
key: sortRecord(op.key),
|
|
1069
|
+
value: op.value ? sortRecord(op.value) : undefined
|
|
1070
|
+
}));
|
|
1071
|
+
}
|
|
1072
|
+
function sortRecord(record) {
|
|
1073
|
+
const out = {};
|
|
1074
|
+
for (const key of Object.keys(record).sort())
|
|
1075
|
+
out[key] = record[key] ?? null;
|
|
1076
|
+
return out;
|
|
1077
|
+
}
|
|
1078
|
+
function hashEffectManifest(ops) {
|
|
1079
|
+
const canonical = canonicalizeMutations(ops);
|
|
1080
|
+
const hash = createHash3("sha256");
|
|
1081
|
+
hash.update(JSON.stringify(canonical));
|
|
1082
|
+
return hash.digest("hex");
|
|
1083
|
+
}
|
|
1084
|
+
function manifestsEqual(a, b) {
|
|
1085
|
+
return hashEffectManifest(a) === hashEffectManifest(b);
|
|
1086
|
+
}
|
|
1087
|
+
// src/coverage/subgraph-adapter.ts
|
|
1088
|
+
var SUBGRAPH_COMMIT_STEPS = [
|
|
1089
|
+
"effects",
|
|
1090
|
+
"rows",
|
|
1091
|
+
"journal",
|
|
1092
|
+
"cursor",
|
|
1093
|
+
"receipt",
|
|
1094
|
+
"failure"
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
class SubgraphAdapterCrash extends Error {
|
|
1098
|
+
step;
|
|
1099
|
+
name = "SubgraphAdapterCrash";
|
|
1100
|
+
constructor(step) {
|
|
1101
|
+
super(`subgraph adapter crash after ${step}`);
|
|
1102
|
+
this.step = step;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async function runSubgraphCommitSteps(steps, crashAfter) {
|
|
1106
|
+
for (const step of SUBGRAPH_COMMIT_STEPS) {
|
|
1107
|
+
await steps[step]();
|
|
1108
|
+
if (crashAfter === step)
|
|
1109
|
+
throw new SubgraphAdapterCrash(step);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async function commitSubgraphAdapter(input, wrap = (fn) => fn()) {
|
|
1113
|
+
await wrap(() => runSubgraphCommitSteps({
|
|
1114
|
+
effects: input.writeEffects,
|
|
1115
|
+
rows: input.writeRows,
|
|
1116
|
+
journal: input.writeJournal,
|
|
1117
|
+
cursor: input.writeCursor,
|
|
1118
|
+
receipt: input.writeReceipts,
|
|
1119
|
+
failure: input.writeFailure
|
|
1120
|
+
}, input.crashAfter));
|
|
1121
|
+
}
|
|
1122
|
+
// src/coverage/sparse.ts
|
|
1123
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1124
|
+
function skipReceipt(skip, chainHash) {
|
|
1125
|
+
const empty = hashEffectManifest([]);
|
|
1126
|
+
return {
|
|
1127
|
+
height: skip.from_height,
|
|
1128
|
+
hash: chainHash,
|
|
1129
|
+
effect_digest: empty,
|
|
1130
|
+
row_digest: empty
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function sequentialDigests(heights) {
|
|
1134
|
+
const ordered = [...heights].sort((a, b) => a.height - b.height);
|
|
1135
|
+
return {
|
|
1136
|
+
chain: rangeDigestOf(ordered.map((h) => `${h.height}:${h.hash}`)),
|
|
1137
|
+
effect: rangeDigestOf(ordered.map((h) => h.effect_digest)),
|
|
1138
|
+
row: rangeDigestOf(ordered.map((h) => h.row_digest))
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
function sparseDigests(executed, skips) {
|
|
1142
|
+
const skipped = skips.flatMap(({ skip, hash }) => {
|
|
1143
|
+
const out = [];
|
|
1144
|
+
for (let h = skip.from_height;h <= skip.to_height; h++) {
|
|
1145
|
+
out.push(skipReceipt({ ...skip, from_height: h, to_height: h }, hash));
|
|
1146
|
+
}
|
|
1147
|
+
return out;
|
|
1148
|
+
});
|
|
1149
|
+
return sequentialDigests([...executed, ...skipped]);
|
|
1150
|
+
}
|
|
1151
|
+
function proofsAgree(sequential, sparse) {
|
|
1152
|
+
return sequential.chain === sparse.chain && sequential.effect === sparse.effect && sequential.row === sparse.row;
|
|
1153
|
+
}
|
|
1154
|
+
// src/coverage/deep-verify.ts
|
|
1155
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
1156
|
+
function finalRowDigest(rows) {
|
|
1157
|
+
const ordered = [...rows].sort((a, b) => a.table.localeCompare(b.table) || a.key.localeCompare(b.key));
|
|
1158
|
+
const hash = createHash5("sha256");
|
|
1159
|
+
for (const row of ordered) {
|
|
1160
|
+
hash.update(`${row.table} ${row.key} ${row.value}
|
|
1161
|
+
`);
|
|
1162
|
+
}
|
|
1163
|
+
return hash.digest("hex");
|
|
1164
|
+
}
|
|
1165
|
+
function applyMutations(rows, ops) {
|
|
1166
|
+
const map = new Map(rows.map((r) => [`${r.table}:${r.key}`, { ...r }]));
|
|
1167
|
+
for (const op of ops) {
|
|
1168
|
+
const id = String(op.key.id ?? JSON.stringify(op.key));
|
|
1169
|
+
const k = `${op.table}:${id}`;
|
|
1170
|
+
if (op.op === "delete") {
|
|
1171
|
+
map.delete(k);
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
map.set(k, {
|
|
1175
|
+
table: op.table,
|
|
1176
|
+
key: id,
|
|
1177
|
+
value: JSON.stringify(op.value ?? {})
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
return [...map.values()];
|
|
1181
|
+
}
|
|
1182
|
+
function deepVerify(opts) {
|
|
1183
|
+
const scratch = applyMutations([], opts.replay);
|
|
1184
|
+
const liveDigest = finalRowDigest(opts.live);
|
|
1185
|
+
const scratchDigest = finalRowDigest(scratch);
|
|
1186
|
+
let found = false;
|
|
1187
|
+
if (opts.seed) {
|
|
1188
|
+
const tainted = applyMutations(opts.live, [opts.seed]);
|
|
1189
|
+
found = finalRowDigest(tainted) !== liveDigest;
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
ok: liveDigest === scratchDigest,
|
|
1193
|
+
live_digest: liveDigest,
|
|
1194
|
+
scratch_digest: scratchDigest,
|
|
1195
|
+
found_mutation: found
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
// src/coverage/queue.ts
|
|
1199
|
+
var QUEUE_STAGES = [
|
|
1200
|
+
"decision",
|
|
1201
|
+
"outbox",
|
|
1202
|
+
"delivery",
|
|
1203
|
+
"rollback",
|
|
1204
|
+
"dlq",
|
|
1205
|
+
"replay"
|
|
1206
|
+
];
|
|
1207
|
+
function emptyQueueState() {
|
|
1208
|
+
return {
|
|
1209
|
+
stage: "decision",
|
|
1210
|
+
accepted: 0,
|
|
1211
|
+
decided: 0,
|
|
1212
|
+
enqueued: 0,
|
|
1213
|
+
delivered: 0,
|
|
1214
|
+
dead: 0,
|
|
1215
|
+
fence_cursor: null,
|
|
1216
|
+
last_cursor: null,
|
|
1217
|
+
dedupe: new Set
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function applyQueueEvent(state, event, dedupeKey) {
|
|
1221
|
+
if (dedupeKey && state.dedupe.has(dedupeKey)) {
|
|
1222
|
+
return { ok: true, state };
|
|
1223
|
+
}
|
|
1224
|
+
const next = {
|
|
1225
|
+
...state,
|
|
1226
|
+
dedupe: new Set(state.dedupe)
|
|
1227
|
+
};
|
|
1228
|
+
if (dedupeKey)
|
|
1229
|
+
next.dedupe.add(dedupeKey);
|
|
1230
|
+
switch (event.type) {
|
|
1231
|
+
case "accept":
|
|
1232
|
+
next.accepted += 1;
|
|
1233
|
+
next.stage = "decision";
|
|
1234
|
+
return { ok: true, state: next };
|
|
1235
|
+
case "decide":
|
|
1236
|
+
if (next.decided >= next.accepted) {
|
|
1237
|
+
return { ok: false, reason: "decide without accept", state };
|
|
1238
|
+
}
|
|
1239
|
+
next.decided += 1;
|
|
1240
|
+
next.stage = event.matched ? "outbox" : "decision";
|
|
1241
|
+
return { ok: true, state: next };
|
|
1242
|
+
case "enqueue":
|
|
1243
|
+
if (next.enqueued >= next.decided) {
|
|
1244
|
+
return { ok: false, reason: "enqueue without decide", state };
|
|
1245
|
+
}
|
|
1246
|
+
next.enqueued += 1;
|
|
1247
|
+
next.stage = "outbox";
|
|
1248
|
+
return { ok: true, state: next };
|
|
1249
|
+
case "deliver":
|
|
1250
|
+
if (next.delivered + next.dead >= next.enqueued) {
|
|
1251
|
+
return { ok: false, reason: "deliver without enqueue", state };
|
|
1252
|
+
}
|
|
1253
|
+
next.delivered += 1;
|
|
1254
|
+
next.stage = "delivery";
|
|
1255
|
+
return { ok: true, state: next };
|
|
1256
|
+
case "fail":
|
|
1257
|
+
next.stage = "delivery";
|
|
1258
|
+
return { ok: true, state: next };
|
|
1259
|
+
case "dead":
|
|
1260
|
+
if (next.delivered + next.dead >= next.enqueued) {
|
|
1261
|
+
return { ok: false, reason: "dead without enqueue", state };
|
|
1262
|
+
}
|
|
1263
|
+
next.dead += 1;
|
|
1264
|
+
next.stage = "dlq";
|
|
1265
|
+
return { ok: true, state: next };
|
|
1266
|
+
case "requeue":
|
|
1267
|
+
if (next.dead < 1)
|
|
1268
|
+
return { ok: false, reason: "requeue empty dlq", state };
|
|
1269
|
+
next.dead -= 1;
|
|
1270
|
+
next.enqueued += 1;
|
|
1271
|
+
next.stage = "outbox";
|
|
1272
|
+
return { ok: true, state: next };
|
|
1273
|
+
case "rollback":
|
|
1274
|
+
next.stage = "rollback";
|
|
1275
|
+
return { ok: true, state: next };
|
|
1276
|
+
case "replay":
|
|
1277
|
+
next.stage = "replay";
|
|
1278
|
+
next.last_cursor = event.cursor;
|
|
1279
|
+
next.fence_cursor = event.cursor;
|
|
1280
|
+
return { ok: true, state: next };
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function queueCaughtUp(state) {
|
|
1284
|
+
return state.accepted === state.decided && state.delivered + state.dead === state.enqueued;
|
|
1285
|
+
}
|
|
1286
|
+
// src/coverage/repair.ts
|
|
1287
|
+
var SAFE_REPAIR_MODES = [
|
|
1288
|
+
"range_safe",
|
|
1289
|
+
"full_reindex",
|
|
1290
|
+
"archive_replay"
|
|
1291
|
+
];
|
|
1292
|
+
function planRepair(input) {
|
|
1293
|
+
if (input.range.from_height > input.range.to_height) {
|
|
1294
|
+
return {
|
|
1295
|
+
...input,
|
|
1296
|
+
safe: false,
|
|
1297
|
+
reason: "inverted range"
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
if (!SAFE_REPAIR_MODES.includes(input.mode)) {
|
|
1301
|
+
return {
|
|
1302
|
+
...input,
|
|
1303
|
+
safe: false,
|
|
1304
|
+
reason: `mode ${input.mode} is not registered`
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
if (input.mode === "range_safe" && !input.range_safe_proven) {
|
|
1308
|
+
return {
|
|
1309
|
+
...input,
|
|
1310
|
+
safe: false,
|
|
1311
|
+
reason: "range_safe requires a proven range"
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
if (input.defect === "source_gap" && input.mode !== "archive_replay") {
|
|
1315
|
+
return {
|
|
1316
|
+
...input,
|
|
1317
|
+
safe: false,
|
|
1318
|
+
reason: "raw/source gaps require archive_replay"
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
...input,
|
|
1323
|
+
safe: true,
|
|
1324
|
+
reason: `dry-run ${input.mode} for ${input.defect}`
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function applyRepair(plan, opts) {
|
|
1328
|
+
if (!plan.safe) {
|
|
1329
|
+
return { ok: false, reason: plan.reason, plan };
|
|
1330
|
+
}
|
|
1331
|
+
if (!opts?.apply) {
|
|
1332
|
+
return { ok: true, applied: false, plan };
|
|
1333
|
+
}
|
|
1334
|
+
return { ok: true, applied: true, plan };
|
|
1335
|
+
}
|
|
1336
|
+
// src/coverage/verify-target.ts
|
|
1337
|
+
var VERIFY_EXIT = {
|
|
1338
|
+
CLEAN: 0,
|
|
1339
|
+
DIVERGED: 1,
|
|
1340
|
+
UNANCHORED: 2
|
|
1341
|
+
};
|
|
1342
|
+
function parseVerifyTarget(value) {
|
|
1343
|
+
const raw = (value ?? "raw").trim();
|
|
1344
|
+
if (raw === "all")
|
|
1345
|
+
return { kind: "all" };
|
|
1346
|
+
if (raw === "raw" || raw === "")
|
|
1347
|
+
return { kind: "raw" };
|
|
1348
|
+
if (raw.startsWith("decode:")) {
|
|
1349
|
+
const name = raw.slice("decode:".length).trim();
|
|
1350
|
+
if (!name)
|
|
1351
|
+
throw new Error("decode: target needs a decoder name");
|
|
1352
|
+
return { kind: "decode", name };
|
|
1353
|
+
}
|
|
1354
|
+
if (raw.startsWith("subgraph:")) {
|
|
1355
|
+
const name = raw.slice("subgraph:".length).trim();
|
|
1356
|
+
if (!name)
|
|
1357
|
+
throw new Error("subgraph: target needs a subgraph name");
|
|
1358
|
+
return { kind: "subgraph", name };
|
|
1359
|
+
}
|
|
1360
|
+
throw new Error(`verify target must be all | raw | decode:<name> | subgraph:<name> (got ${value})`);
|
|
1361
|
+
}
|
|
1362
|
+
var RAW_DATASETS = new Set(["blocks", "transactions", "events"]);
|
|
1363
|
+
function datasetMatchesTarget(dataset, target) {
|
|
1364
|
+
if (target.kind === "all")
|
|
1365
|
+
return true;
|
|
1366
|
+
const name = dataset.toLowerCase();
|
|
1367
|
+
if (target.kind === "raw")
|
|
1368
|
+
return RAW_DATASETS.has(name);
|
|
1369
|
+
if (target.kind === "decode") {
|
|
1370
|
+
return name === target.name.toLowerCase() || name === `decode:${target.name.toLowerCase()}` || name.endsWith(`_${target.name.toLowerCase()}`);
|
|
1371
|
+
}
|
|
1372
|
+
return name === target.name.toLowerCase() || name === `subgraph:${target.name.toLowerCase()}` || name.startsWith(`${target.name.toLowerCase()}_`);
|
|
1373
|
+
}
|
|
1374
|
+
function reportVerify(opts) {
|
|
1375
|
+
const mode = opts.mode ?? "quick";
|
|
1376
|
+
if (opts.anchored === false) {
|
|
1377
|
+
return {
|
|
1378
|
+
target: opts.target,
|
|
1379
|
+
mode,
|
|
1380
|
+
exit: VERIFY_EXIT.UNANCHORED,
|
|
1381
|
+
status: "unanchored",
|
|
1382
|
+
detail: opts.detail ?? "reference unavailable or unverifiable"
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
if (opts.diverged) {
|
|
1386
|
+
return {
|
|
1387
|
+
target: opts.target,
|
|
1388
|
+
mode,
|
|
1389
|
+
exit: VERIFY_EXIT.DIVERGED,
|
|
1390
|
+
status: "diverged",
|
|
1391
|
+
detail: opts.detail ?? "coverage or archive diverged"
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
const label = opts.target.kind === "decode" ? `decode:${opts.target.name}` : opts.target.kind === "subgraph" ? `subgraph:${opts.target.name}` : opts.target.kind;
|
|
1395
|
+
return {
|
|
1396
|
+
target: opts.target,
|
|
1397
|
+
mode,
|
|
1398
|
+
exit: VERIFY_EXIT.CLEAN,
|
|
1399
|
+
status: "clean",
|
|
1400
|
+
detail: opts.detail ?? `${label} ${mode} is clean`
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
export {
|
|
1404
|
+
versionsEqual,
|
|
1405
|
+
topoSort,
|
|
1406
|
+
sparseDigests,
|
|
1407
|
+
sequentialDigests,
|
|
1408
|
+
segmentsSurviveReorg,
|
|
1409
|
+
segmentsOverlap,
|
|
1410
|
+
sealFinalizedRange,
|
|
1411
|
+
runSubgraphCommitSteps,
|
|
1412
|
+
runDecoderCommitSteps,
|
|
1413
|
+
reportVerify,
|
|
1414
|
+
receiptsAfterReorg,
|
|
1415
|
+
receiptRetentionHolds,
|
|
1416
|
+
rangeIsOrdered,
|
|
1417
|
+
rangeDigestOf,
|
|
1418
|
+
queueCaughtUp,
|
|
1419
|
+
proofsAgree,
|
|
1420
|
+
planRepair,
|
|
1421
|
+
planDecoderReceipts,
|
|
1422
|
+
parseVerifyTarget,
|
|
1423
|
+
mergeRanges,
|
|
1424
|
+
manifestsEqual,
|
|
1425
|
+
lookupTransition,
|
|
1426
|
+
isStageKind,
|
|
1427
|
+
isRunStatus,
|
|
1428
|
+
isRetryState,
|
|
1429
|
+
isRepairMode,
|
|
1430
|
+
isNonNegativeInt,
|
|
1431
|
+
isNativeClock,
|
|
1432
|
+
isFailureUnit,
|
|
1433
|
+
isFailureClass,
|
|
1434
|
+
inputDigest,
|
|
1435
|
+
hashEffectManifest,
|
|
1436
|
+
findRangeGaps,
|
|
1437
|
+
finalRowDigest,
|
|
1438
|
+
failureRetentionHolds,
|
|
1439
|
+
failureRangeHolds,
|
|
1440
|
+
expectedAckHeight,
|
|
1441
|
+
evaluateCoverage,
|
|
1442
|
+
emptyQueueState,
|
|
1443
|
+
defaultRetainUntil,
|
|
1444
|
+
deepVerify,
|
|
1445
|
+
datasetMatchesTarget,
|
|
1446
|
+
cursorIsBeforeBlock,
|
|
1447
|
+
cursorIsAfterBlock,
|
|
1448
|
+
cursorHeight,
|
|
1449
|
+
createRunnerState,
|
|
1450
|
+
contiguousThrough,
|
|
1451
|
+
commitSubgraphAdapter,
|
|
1452
|
+
commitDecoderAdapter,
|
|
1453
|
+
canonicalizeMutations,
|
|
1454
|
+
applyRunnerEvent,
|
|
1455
|
+
applyRepair,
|
|
1456
|
+
applyQueueEvent,
|
|
1457
|
+
applyMutations,
|
|
1458
|
+
applyDecoderReceipts,
|
|
1459
|
+
VERIFY_EXIT,
|
|
1460
|
+
SubgraphAdapterCrash,
|
|
1461
|
+
SUBGRAPH_COMMIT_STEPS,
|
|
1462
|
+
STAGE_KINDS,
|
|
1463
|
+
SAFE_REPAIR_MODES,
|
|
1464
|
+
RUN_STATUSES,
|
|
1465
|
+
RUNNER_TRANSITION_TABLE,
|
|
1466
|
+
RUNNER_STATUSES,
|
|
1467
|
+
RUNNER_EVENT_TYPES,
|
|
1468
|
+
RETRY_STATES,
|
|
1469
|
+
REPAIR_MODES,
|
|
1470
|
+
QUEUE_STAGES,
|
|
1471
|
+
NATIVE_CLOCKS,
|
|
1472
|
+
FAILURE_UNITS,
|
|
1473
|
+
FAILURE_RETENTION_DAYS,
|
|
1474
|
+
FAILURE_CLASSES,
|
|
1475
|
+
DecoderAdapterCrash,
|
|
1476
|
+
DEFAULT_MAX_RETRIES,
|
|
1477
|
+
DECODER_COMMIT_STEPS,
|
|
1478
|
+
COVERAGE_STATES,
|
|
1479
|
+
COVERAGE_REPORT_SCHEMA_VERSION
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
//# debugId=74D0FA2357387FF664756E2164756E21
|
|
1483
|
+
//# sourceMappingURL=index.js.map
|