@sanning/proof 0.14.0 → 0.16.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.
@@ -0,0 +1,300 @@
1
+ // The public-record (witness) lookup the CLI runs with `--gateways`
2
+ // (evidence-bundle.md §5.5.6, v1.20, A24, D7): re-fetch each pack's witness on
3
+ // the public record and report what it said. Moved here from `cli.ts`
4
+ // (proof#200) so the CLI and a consumer that is not the CLI, the verify page,
5
+ // a browser app, call the SAME implementation rather than each keeping its
6
+ // own copy that can drift.
7
+ //
8
+ // Pure by the kernel's own rule (specs/architecture.md §3): the network call
9
+ // is the one exception this module exists to hold, and it never runs unless a
10
+ // caller hands in a gateway to ask. `options.fetchImpl` is the seam: where a
11
+ // caller supplies one (a browser page's own `fetch`, or a test's fake), that
12
+ // is the ONE this module calls; where none is given, it falls back to the
13
+ // ambient global `fetch`, the same fallback `cli.ts` already applied at its
14
+ // own call site before this module existed.
15
+ //
16
+ // `onChainHalf` and `witnessLine` are the CLI's own building blocks,
17
+ // UNCHANGED (proof#200 is additive: the CLI's `case` output and exit codes are
18
+ // byte-for-byte what they were). `lookupPublicRecord` is the new export:
19
+ // one pack (or several) in, one four-state result out, with a pack's
20
+ // `witness-cache.json` read as part of the pack rather than as a side file a
21
+ // caller must remember to also pass in.
22
+ import { verifyEvidenceBundle } from "./evidence.js";
23
+ import { WITNESS_NOT_LOOKED_UP, renderWitnessLine } from "./case.js";
24
+ import { checkWitnessCache, readWitnessCache, renderWitnessTime, } from "./witness-cache.js";
25
+ function witnessTimeRow(t) {
26
+ return {
27
+ stamp_ref: t.cached.stampRef,
28
+ cached: { block_height: t.cached.blockHeight, block_time: t.cached.blockTime },
29
+ outcome: t.outcome,
30
+ answers: t.answers.map((a) => ({
31
+ gateway: a.gateway,
32
+ outcome: a.outcome,
33
+ block_height: a.blockHeight,
34
+ block_time: a.blockTime,
35
+ })),
36
+ };
37
+ }
38
+ /**
39
+ * Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside
40
+ * the case reading on purpose: `readCase` makes no network call, and neither
41
+ * does the verify page's case reading, so the case verdict stays the one both
42
+ * surfaces compute. This adds the half the reading names as not checked.
43
+ *
44
+ * Moved from `cli.ts` unchanged, apart from taking `fetchImpl` directly
45
+ * instead of a `Cli` stream (`io.fetchImpl ?? fetch`, the same fallback the
46
+ * CLI already applied at its own call site).
47
+ */
48
+ export async function onChainHalf(packs, gateways, options = {}) {
49
+ if (gateways.length === 0)
50
+ return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
51
+ const fetchImpl = options.fetchImpl ?? fetch;
52
+ const rows = [];
53
+ const answered = new Set();
54
+ for (const [ref, bundle, cached] of packs) {
55
+ const result = await verifyEvidenceBundle(bundle, { gateways: [...gateways], fetchImpl });
56
+ const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
57
+ const row = {
58
+ ref,
59
+ witnesses: witnesses.length,
60
+ confirmed: 0,
61
+ mismatched: 0,
62
+ unreachable: 0,
63
+ not_checked: 0,
64
+ };
65
+ for (const w of witnesses) {
66
+ const rollup = w.onChain?.rollup ?? null;
67
+ if (rollup === "confirm")
68
+ row.confirmed++;
69
+ else if (rollup === "mismatch")
70
+ row.mismatched++;
71
+ else if (rollup === "unreachable")
72
+ row.unreachable++;
73
+ else
74
+ row.not_checked++;
75
+ }
76
+ // The witness times the pack's witness-cache.json gives, looked up where
77
+ // it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
78
+ // no request.
79
+ const times = await checkWitnessCache(cached, gateways, fetchImpl);
80
+ if (times.length > 0)
81
+ row.witness_times = times.map(witnessTimeRow);
82
+ rows.push(row);
83
+ for (const w of witnesses) {
84
+ for (const outcome of w.onChain?.perGateway ?? []) {
85
+ if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
86
+ answered.add(outcome.gateway);
87
+ }
88
+ }
89
+ }
90
+ for (const t of times) {
91
+ for (const a of t.answers) {
92
+ if (a.outcome === "matches" || a.outcome === "differs")
93
+ answered.add(a.gateway);
94
+ }
95
+ }
96
+ }
97
+ const verdict = rows.some((r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"))
98
+ ? "failed"
99
+ : rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)
100
+ ? "undetermined"
101
+ : "confirmed";
102
+ return {
103
+ answered: gateways.filter((g) => answered.has(g)),
104
+ checked: true,
105
+ gateways: [...gateways],
106
+ packs: rows,
107
+ verdict,
108
+ };
109
+ }
110
+ // Distinct `[blockHeight, blockTime]` pairs, in time order.
111
+ function distinctTimes(times) {
112
+ const seen = new Map();
113
+ for (const [h, t] of times)
114
+ seen.set(`${h}:${t}`, [h, t]);
115
+ return [...seen.values()].sort((a, b) => a[1] - b[1] || a[0] - b[0]);
116
+ }
117
+ // Only the keys that carry something: a line with no cached or recorded time
118
+ // is the line 0.13.0 printed, field for field.
119
+ function withTimes(line, cached, recorded) {
120
+ return {
121
+ ...line,
122
+ ...(cached.length > 0 ? { cached } : {}),
123
+ ...(recorded.length > 0 ? { recorded } : {}),
124
+ };
125
+ }
126
+ /**
127
+ * `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
128
+ * lookup ran, each is printed labelled not checked. Where one ran, each time
129
+ * the public record matched is printed as the record's, each time no lookup
130
+ * settled stays labelled not checked, and a time the record contradicts is the
131
+ * reason the line reads `differs`.
132
+ *
133
+ * Moved from `cli.ts` unchanged: same inputs, same three states
134
+ * (`WitnessState`), same rendering. The CLI's `case` command still calls this
135
+ * exact function, so its witness line is unchanged.
136
+ */
137
+ export function witnessLine(onChain, cached = []) {
138
+ if (!onChain.checked) {
139
+ return withTimes({
140
+ state: WITNESS_NOT_LOOKED_UP.state,
141
+ reasons: [...WITNESS_NOT_LOOKED_UP.reasons],
142
+ services: [...WITNESS_NOT_LOOKED_UP.services],
143
+ }, distinctTimes(cached.map((c) => [c.blockHeight, c.blockTime])), []);
144
+ }
145
+ const services = [...(onChain.answered ?? [])];
146
+ const rows = onChain.packs;
147
+ const times = rows.flatMap((r) => r.witness_times ?? []);
148
+ const unsettled = distinctTimes(times
149
+ .filter((t) => t.outcome === "not_checked")
150
+ .map((t) => [t.cached.block_height, t.cached.block_time]));
151
+ const recorded = distinctTimes(times
152
+ .filter((t) => t.outcome === "matches")
153
+ .flatMap((t) => t.answers.filter((a) => a.outcome === "matches"))
154
+ .map((a) => [a.block_height, a.block_time]));
155
+ const contradicted = [];
156
+ for (const t of times) {
157
+ if (t.outcome !== "differs")
158
+ continue;
159
+ const a = t.answers.find((x) => x.outcome === "differs");
160
+ const reason = `a pack dates the witness ${renderWitnessTime(t.cached.block_height, t.cached.block_time)}` +
161
+ `, and the public record dates it ${renderWitnessTime(a.block_height, a.block_time)}`;
162
+ if (!contradicted.includes(reason))
163
+ contradicted.push(reason);
164
+ }
165
+ if (onChain.verdict === "failed") {
166
+ return withTimes({ state: "differs", reasons: contradicted, services }, unsettled, recorded);
167
+ }
168
+ if (onChain.verdict === "confirmed") {
169
+ return withTimes({ state: "confirmed", reasons: [], services }, unsettled, recorded);
170
+ }
171
+ const reasons = [];
172
+ if (rows.some((r) => r.witnesses === 0))
173
+ reasons.push("a pack names no witness");
174
+ if (rows.some((r) => r.unreachable > 0)) {
175
+ reasons.push("a lookup did not settle: the service could not be reached, or it " + "holds no such witness yet");
176
+ }
177
+ if (rows.some((r) => r.not_checked > 0)) {
178
+ reasons.push("no lookup is made for a witness kind a pack names");
179
+ }
180
+ return withTimes({ state: "not_checked", reasons, services }, unsettled, recorded);
181
+ }
182
+ /**
183
+ * The public-record lookup the CLI runs with `--gateways`, exported so a
184
+ * consumer that is not the CLI can run the SAME lookup rather than copy it
185
+ * (proof#200; the copy this closes is verify-page#70's `src/lib/verify.ts`).
186
+ *
187
+ * `packs` is every pack in the drop being read; `gateways` is empty for an
188
+ * offline reading, which never makes a network call and always answers
189
+ * `not_checked` or `no_public_record`. `options.fetchImpl` is the one seam a
190
+ * network call goes through, a browser page supplies its own `fetchImpl`;
191
+ * with none given this falls back to the ambient global `fetch`, so a
192
+ * browser page and a test can each supply their own and Node's `fetch` is
193
+ * never assumed where a caller passed something else.
194
+ *
195
+ * `state` is `confirmed` only about the BYTES: the public record holds what a
196
+ * pack names. It says nothing about whether the pack itself verifies (its
197
+ * wrapper signature, its `body_hash`), and a caller must never show
198
+ * `confirmed` apart from the pack's own verdict (proof#201).
199
+ *
200
+ * Never throws. Every input, including a malformed pack, a non-string
201
+ * gateway, or a `fetchImpl` that itself misbehaves, is read as untrusted, and
202
+ * anything this function or what it calls cannot make sense of settles as
203
+ * `not_checked` rather than escape as an exception.
204
+ */
205
+ export async function lookupPublicRecord(packs, gateways, options = {}) {
206
+ try {
207
+ // Nothing to look up: zero packs means the CALLER handed in nothing to
208
+ // check, which is not the same claim as "the packs it handed in name no
209
+ // witness" (`no_public_record`, below). Checked first, and unconditionally,
210
+ // so a caller than mishandles an empty drop never reads it as confirmed.
211
+ if (packs.length === 0) {
212
+ const reasons = [...WITNESS_NOT_LOOKED_UP.reasons];
213
+ return {
214
+ state: "not_checked",
215
+ reasons,
216
+ services: [],
217
+ rendered: renderWitnessLine({ state: "not_checked", reasons, services: [] }),
218
+ };
219
+ }
220
+ const caches = packs.map((p) => readWitnessCache(p.bundle, p.witnessCache ?? null));
221
+ const rows = packs.map((p, i) => [
222
+ p.ref,
223
+ p.bundle,
224
+ caches[i],
225
+ ]);
226
+ const onChain = await onChainHalfForWitnessCount(rows, gateways, options);
227
+ const line = witnessLine(onChain.half, caches.flat());
228
+ // "No public record yet": every pack this lookup was handed names ZERO
229
+ // witnesses, so there is nothing the public record could confirm or
230
+ // contradict. This holds independently of whether a lookup ran, an
231
+ // offline reading of a fresh, unstamped pack is exactly as much "nothing to
232
+ // check yet" as an online one, and it is computed from the SAME witness
233
+ // count `onChainHalf` derives from `verifyEvidenceBundle`'s own parse, never
234
+ // a second reading of the bundle.
235
+ //
236
+ // A pack that does NOT verify (a broken wrapper signature, `bundle: null`,
237
+ // garbage, an unrecognised body type) also names zero witnesses, by
238
+ // construction: `verifyEvidenceBundle` never reports a checkpoint or a
239
+ // stamp for a bundle it could not authenticate. That is not "nothing to
240
+ // check yet", it is a pack this lookup could not read, so `no_public_record`
241
+ // is reserved for a pack whose wrapper genuinely verifies
242
+ // (`signatureOk && bodyHashOk && status !== "failed"`) and that carries no
243
+ // cached witness time either (a cache entry names something to check).
244
+ // Anything else that would otherwise qualify reads `not_checked` instead.
245
+ const noWitnesses = onChain.witnessCounts.every((n) => n === 0);
246
+ const everyPackVerifies = onChain.verifies.every((v) => v);
247
+ const noCacheRead = caches.every((c) => c.length === 0);
248
+ const canBeNoPublicRecord = noWitnesses && everyPackVerifies && noCacheRead;
249
+ const state = line.state === "not_checked" && canBeNoPublicRecord ? "no_public_record" : line.state;
250
+ return {
251
+ state,
252
+ reasons: line.reasons,
253
+ services: line.services,
254
+ ...(line.cached ? { cached: line.cached } : {}),
255
+ // `recorded` is a claim about the public record confirming a cached
256
+ // time; it never rides alongside anything but a `confirmed` verdict, so
257
+ // a pack whose wrapper does not verify can never surface a cached time
258
+ // as though the record had confirmed it.
259
+ ...(state === "confirmed" && line.recorded ? { recorded: line.recorded } : {}),
260
+ rendered: renderWitnessLine({ ...line, state: state === "no_public_record" ? "not_checked" : state }),
261
+ };
262
+ }
263
+ catch (e) {
264
+ // Untrusted input all the way down: a gateway's answer, a pack's shape, a
265
+ // caller's `fetchImpl`. Anything this function or what it calls cannot
266
+ // make sense of settles `not_checked` rather than throw out of an export a
267
+ // browser page calls directly.
268
+ const reason = e instanceof Error ? `the lookup did not settle: ${e.message}` : "the lookup did not settle";
269
+ const reasons = [reason];
270
+ return {
271
+ state: "not_checked",
272
+ reasons,
273
+ services: [],
274
+ rendered: renderWitnessLine({ state: "not_checked", reasons, services: [] }),
275
+ };
276
+ }
277
+ }
278
+ // `onChainHalf` short-circuits to `{ checked: false, packs: [] }` with no
279
+ // gateways, which is right for the CLI (nothing to print per pack) but leaves
280
+ // `lookupPublicRecord` unable to tell "no gateways" from "no witnesses" for an
281
+ // OFFLINE reading. This wrapper reads each pack's witness count, and whether
282
+ // its wrapper verifies, the same way `onChainHalf` does, via
283
+ // `verifyEvidenceBundle`: an OFFLINE parse (`gateways: []`), so it costs no
284
+ // extra request whether or not a lookup runs, and `verifies` is never itself
285
+ // a statement about the public record, only about the pack's own signature
286
+ // and `body_hash`.
287
+ async function onChainHalfForWitnessCount(packs, gateways, options) {
288
+ const witnessCounts = [];
289
+ const verifies = [];
290
+ for (const [, bundle] of packs) {
291
+ const result = await verifyEvidenceBundle(bundle, { gateways: [] });
292
+ witnessCounts.push(result.checkpoints.length + (result.stamps?.length ?? 0));
293
+ verifies.push(result.signatureOk && result.bodyHashOk && result.status !== "failed");
294
+ }
295
+ if (gateways.length > 0) {
296
+ const half = await onChainHalf(packs, gateways, options);
297
+ return { half, witnessCounts, verifies };
298
+ }
299
+ return { half: { checked: false, gateways: [], packs: [], verdict: "not_checked" }, witnessCounts, verifies };
300
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanning/proof",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@types/node": "^20.19.41",
38
38
  "typescript": "^5.4.0",
39
- "vitest": "^2.0.0"
39
+ "vitest": "^4.1.11"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public"
package/src/cli.ts CHANGED
@@ -58,8 +58,6 @@ type ChainOutcome = ChainReading | { unreadable: string };
58
58
  import type { ChainReading } from "./chain.js";
59
59
  import {
60
60
  VERDICT_DISPLAY_TITLES,
61
- WITNESS_NOT_LOOKED_UP,
62
- type WitnessLine,
63
61
  caseReliance,
64
62
  readCase,
65
63
  renderCaseNote,
@@ -83,13 +81,15 @@ import { fileMatchToJson, matchCaseFiles, matchFiles, renderFileMatch } from "./
83
81
  import type { OfferedFile } from "./file-match.js";
84
82
  import {
85
83
  type CachedWitnessTime,
86
- type WitnessTimeAnswer,
87
- type WitnessTimeCheck,
88
84
  WITNESS_CACHE_NAME,
89
- checkWitnessCache,
90
85
  readWitnessCache,
91
- renderWitnessTime,
92
86
  } from "./witness-cache.js";
87
+ // The public-record (witness) lookup this command runs with `--gateways`:
88
+ // moved to its own pure module (proof#200) so a consumer that is not this CLI,
89
+ // the verify page, can call the SAME implementation. `onChainHalf` and
90
+ // `witnessLine` are this command's own two building blocks, unchanged; see
91
+ // `./witness-lookup.ts` for the exported entry point, `lookupPublicRecord`.
92
+ import { type OnChainHalf, onChainHalf, witnessLine } from "./witness-lookup.js";
93
93
  import type { AttestationRecord, EvidenceBundle, EvidenceBundleResult, ExportResult } from "./evidence.js";
94
94
  import { checkDeclaration, renderDeclarationSummary, renderNote } from "./declaration.js";
95
95
  import { completenessNotes, gradePack, renderCause } from "./grading.js";
@@ -2033,208 +2033,6 @@ function jsonText(v: unknown): string {
2033
2033
  return JSON.stringify(sortKeys(v), null, 2);
2034
2034
  }
2035
2035
 
2036
- interface OnChainRow {
2037
- ref: string;
2038
- witnesses: number;
2039
- confirmed: number;
2040
- mismatched: number;
2041
- unreachable: number;
2042
- not_checked: number;
2043
- /** Present only where the pack caches a witness time (5.6.6, v1.25, A30). */
2044
- witness_times?: WitnessTimeRow[];
2045
- }
2046
-
2047
- interface WitnessTimeRow {
2048
- stamp_ref: string;
2049
- cached: { block_height: number; block_time: number };
2050
- outcome: WitnessTimeCheck["outcome"];
2051
- answers: {
2052
- gateway: string;
2053
- outcome: WitnessTimeAnswer["outcome"];
2054
- block_height: number | null;
2055
- block_time: number | null;
2056
- }[];
2057
- }
2058
-
2059
- function witnessTimeRow(t: WitnessTimeCheck): WitnessTimeRow {
2060
- return {
2061
- stamp_ref: t.cached.stampRef,
2062
- cached: { block_height: t.cached.blockHeight, block_time: t.cached.blockTime },
2063
- outcome: t.outcome,
2064
- answers: t.answers.map((a) => ({
2065
- gateway: a.gateway,
2066
- outcome: a.outcome,
2067
- block_height: a.blockHeight,
2068
- block_time: a.blockTime,
2069
- })),
2070
- };
2071
- }
2072
-
2073
- interface OnChainHalf {
2074
- /** Present only where a lookup ran: each gateway that answered, in the order given. */
2075
- answered?: string[];
2076
- checked: boolean;
2077
- gateways: string[];
2078
- packs: OnChainRow[];
2079
- verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
2080
- }
2081
-
2082
- // Each pack's witnesses, re-fetched through `verifyEvidenceBundle`. Outside the
2083
- // case reading on purpose: `readCase` makes no network call, and neither does
2084
- // the verify page's case reading, so the case verdict stays the one both
2085
- // surfaces compute. This adds the half the reading names as not checked.
2086
- async function onChainHalf(
2087
- packs: readonly [string, unknown, readonly CachedWitnessTime[]][],
2088
- gateways: string[],
2089
- io: Cli,
2090
- ): Promise<OnChainHalf> {
2091
- if (gateways.length === 0) return { checked: false, gateways: [], packs: [], verdict: "not_checked" };
2092
- const rows: OnChainRow[] = [];
2093
- const answered = new Set<string>();
2094
- for (const [ref, bundle, cached] of packs) {
2095
- const result = await verifyEvidenceBundle(bundle, {
2096
- gateways,
2097
- ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
2098
- });
2099
- const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
2100
- const row: OnChainRow = {
2101
- ref,
2102
- witnesses: witnesses.length,
2103
- confirmed: 0,
2104
- mismatched: 0,
2105
- unreachable: 0,
2106
- not_checked: 0,
2107
- };
2108
- for (const w of witnesses) {
2109
- const rollup = w.onChain?.rollup ?? null;
2110
- if (rollup === "confirm") row.confirmed++;
2111
- else if (rollup === "mismatch") row.mismatched++;
2112
- else if (rollup === "unreachable") row.unreachable++;
2113
- else row.not_checked++;
2114
- }
2115
- // The witness times the pack's witness-cache.json gives, looked up where
2116
- // it gives any (5.6.6, v1.25, A30). A pack with no cache gets no key and
2117
- // no request.
2118
- const times = await checkWitnessCache(cached, gateways, io.fetchImpl ?? fetch);
2119
- if (times.length > 0) row.witness_times = times.map(witnessTimeRow);
2120
- rows.push(row);
2121
- for (const w of witnesses) {
2122
- for (const outcome of w.onChain?.perGateway ?? []) {
2123
- if (outcome.outcome === "confirm" || outcome.outcome === "mismatch") {
2124
- answered.add(outcome.gateway);
2125
- }
2126
- }
2127
- }
2128
- for (const t of times) {
2129
- for (const a of t.answers) {
2130
- if (a.outcome === "matches" || a.outcome === "differs") answered.add(a.gateway);
2131
- }
2132
- }
2133
- }
2134
- const verdict = rows.some(
2135
- (r) => r.mismatched > 0 || (r.witness_times ?? []).some((t) => t.outcome === "differs"),
2136
- )
2137
- ? "failed"
2138
- : rows.some((r) => r.witnesses === 0 || r.unreachable > 0 || r.not_checked > 0)
2139
- ? "undetermined"
2140
- : "confirmed";
2141
- return {
2142
- answered: gateways.filter((g) => answered.has(g)),
2143
- checked: true,
2144
- gateways: [...gateways],
2145
- packs: rows,
2146
- verdict,
2147
- };
2148
- }
2149
-
2150
- /**
2151
- * The witness line (evidence-bundle 5.5.6 v1.20, A24, D7) from the on-chain
2152
- * half: `confirmed` is `confirmed`, `not_checked` and `undetermined` are
2153
- * `not_checked` with the reason, and `failed` is `differs`. It never moves the
2154
- * headline.
2155
- */
2156
- // Distinct `[blockHeight, blockTime]` pairs, in time order.
2157
- function distinctTimes(times: Iterable<[number, number]>): [number, number][] {
2158
- const seen = new Map<string, [number, number]>();
2159
- for (const [h, t] of times) seen.set(`${h}:${t}`, [h, t]);
2160
- return [...seen.values()].sort((a, b) => a[1] - b[1] || a[0] - b[0]);
2161
- }
2162
-
2163
- // Only the keys that carry something: a line with no cached or recorded time
2164
- // is the line 0.13.0 printed, field for field.
2165
- function withTimes(
2166
- line: WitnessLine,
2167
- cached: [number, number][],
2168
- recorded: [number, number][],
2169
- ): WitnessLine {
2170
- return {
2171
- ...line,
2172
- ...(cached.length > 0 ? { cached } : {}),
2173
- ...(recorded.length > 0 ? { recorded } : {}),
2174
- };
2175
- }
2176
-
2177
- /**
2178
- * `cached` is every witness time the packs cache (5.6.6, v1.25, A30). Where no
2179
- * lookup ran, each is printed labelled not checked. Where one ran, each time
2180
- * the public record matched is printed as the record's, each time no lookup
2181
- * settled stays labelled not checked, and a time the record contradicts is the
2182
- * reason the line reads `differs`.
2183
- */
2184
- export function witnessLine(onChain: OnChainHalf, cached: readonly CachedWitnessTime[] = []): WitnessLine {
2185
- if (!onChain.checked) {
2186
- return withTimes(
2187
- {
2188
- state: WITNESS_NOT_LOOKED_UP.state,
2189
- reasons: [...WITNESS_NOT_LOOKED_UP.reasons],
2190
- services: [...WITNESS_NOT_LOOKED_UP.services],
2191
- },
2192
- distinctTimes(cached.map((c): [number, number] => [c.blockHeight, c.blockTime])),
2193
- [],
2194
- );
2195
- }
2196
- const services = [...(onChain.answered ?? [])];
2197
- const rows = onChain.packs;
2198
- const times = rows.flatMap((r) => r.witness_times ?? []);
2199
- const unsettled = distinctTimes(
2200
- times
2201
- .filter((t) => t.outcome === "not_checked")
2202
- .map((t): [number, number] => [t.cached.block_height, t.cached.block_time]),
2203
- );
2204
- const recorded = distinctTimes(
2205
- times
2206
- .filter((t) => t.outcome === "matches")
2207
- .flatMap((t) => t.answers.filter((a) => a.outcome === "matches"))
2208
- .map((a): [number, number] => [a.block_height as number, a.block_time as number]),
2209
- );
2210
- const contradicted: string[] = [];
2211
- for (const t of times) {
2212
- if (t.outcome !== "differs") continue;
2213
- const a = t.answers.find((x) => x.outcome === "differs")!;
2214
- const reason =
2215
- `a pack dates the witness ${renderWitnessTime(t.cached.block_height, t.cached.block_time)}` +
2216
- `, and the public record dates it ${renderWitnessTime(a.block_height as number, a.block_time as number)}`;
2217
- if (!contradicted.includes(reason)) contradicted.push(reason);
2218
- }
2219
- if (onChain.verdict === "failed") {
2220
- return withTimes({ state: "differs", reasons: contradicted, services }, unsettled, recorded);
2221
- }
2222
- if (onChain.verdict === "confirmed") {
2223
- return withTimes({ state: "confirmed", reasons: [], services }, unsettled, recorded);
2224
- }
2225
- const reasons: string[] = [];
2226
- if (rows.some((r) => r.witnesses === 0)) reasons.push("a pack names no witness");
2227
- if (rows.some((r) => r.unreachable > 0)) {
2228
- reasons.push(
2229
- "a lookup did not settle: the service could not be reached, or it " +
2230
- "holds no such witness yet",
2231
- );
2232
- }
2233
- if (rows.some((r) => r.not_checked > 0)) {
2234
- reasons.push("no lookup is made for a witness kind a pack names");
2235
- }
2236
- return withTimes({ state: "not_checked", reasons, services }, unsettled, recorded);
2237
- }
2238
2036
 
2239
2037
  /**
2240
2038
  * The case verdict's code, moved only by an on-chain half that was asked for:
@@ -2513,7 +2311,7 @@ async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
2513
2311
  const onChain = await onChainHalf(
2514
2312
  bundles.map(([p, bundle], i): [string, unknown, CachedWitnessTime[]] => [p.ref, bundle, caches[i]!]),
2515
2313
  gateways,
2516
- io,
2314
+ { ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}) },
2517
2315
  );
2518
2316
  const witness = witnessLine(onChain, caches.flat());
2519
2317
  const code = caseExitCode(reading.verdict, onChain.verdict);