@sanning/proof 0.9.2 → 0.11.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 +7 -7
- package/dist/agent-proof.js +8 -2
- package/dist/case.d.ts +745 -0
- package/dist/case.js +1318 -0
- package/dist/chain.js +24 -5
- package/dist/cli.js +194 -37
- package/dist/compose.js +3 -3
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.js +30 -1
- package/dist/declaration.d.ts +284 -0
- package/dist/declaration.js +674 -0
- package/dist/evidence.d.ts +29 -54
- package/dist/evidence.js +622 -206
- package/dist/grading.d.ts +66 -5
- package/dist/grading.js +134 -9
- package/dist/handoff.d.ts +209 -0
- package/dist/handoff.js +598 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +56 -6
- package/dist/message-parts.d.ts +283 -0
- package/dist/message-parts.js +903 -0
- package/dist/verifier.d.ts +36 -0
- package/dist/verifier.js +157 -6
- package/package.json +2 -2
- package/src/agent-proof.ts +8 -2
- package/src/case.ts +1573 -0
- package/src/chain.ts +24 -5
- package/src/cli.ts +226 -39
- package/src/compose.ts +4 -4
- package/src/crypto.ts +29 -1
- package/src/declaration.ts +1005 -0
- package/src/evidence.ts +707 -259
- package/src/grading.ts +168 -11
- package/src/handoff.ts +674 -0
- package/src/index.ts +132 -8
- package/src/message-parts.ts +998 -0
- package/src/verifier.ts +157 -6
- package/vectors/README.md +37 -2
- package/vectors/chain-reading.json +4 -4
- package/vectors/jcs-reader.json +166 -0
- package/vectors/pack-grading.json +131 -3
- package/vectors/stamp-on-chain.json +3 -3
package/dist/evidence.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Evidence-bundle verification (sanning-io/proof specs/evidence-bundle.md).
|
|
2
2
|
//
|
|
3
3
|
// A `sanning.evidence/v1` bundle is a SIGNED wrapper around a producer-specific
|
|
4
|
-
// `body`. This module verifies the `sanning.anchor.trace/
|
|
4
|
+
// `body`. This module verifies the `sanning.anchor.trace/v3` body, the
|
|
5
5
|
// @sanning/anchor SDK trace: a self-verifying serialization of InclusionReceipt[]
|
|
6
6
|
// (per-event signed envelopes + their checkpoints + RFC 9162 inclusion proofs).
|
|
7
7
|
//
|
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Scope: this verifies a SINGLE evidence bundle. Chain walking over
|
|
17
17
|
// `previous_hash` across successive exports is consumer-layer logic above it.
|
|
18
|
-
import { deriveOperatorAddress, ed25519Verify, hexToBytes,
|
|
18
|
+
import { deriveOperatorAddress, ed25519Verify, hexToBytes,
|
|
19
|
+
// The one hex-field test, which lives in `crypto.ts` beside the one hex
|
|
20
|
+
// reader (proof#146). It was a second copy here until then, with the same
|
|
21
|
+
// body: two copies of one rule is how two kernels drift apart one edit at a
|
|
22
|
+
// time, which is the whole defect class this fix is about.
|
|
23
|
+
isHexField, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
19
24
|
import { leafHash, verifyInclusion } from "./merkle.js";
|
|
20
|
-
import { MAX_CANONICAL_DEPTH, contentHashes, exceedsDepth, jcs, verifyEnvelope, } from "./verifier.js";
|
|
25
|
+
import { MAX_CANONICAL_DEPTH, contentHashes, exceedsDepth, jcs, jcsParse, verifyEnvelope, } from "./verifier.js";
|
|
21
26
|
import { inBodyContentOffered, sideContentEntry, } from "./side-content.js";
|
|
22
27
|
// Only the evidence-bundle major(s) this kernel verifies. A new major is a
|
|
23
28
|
// deliberate one-entry addition HERE — same fail-closed discipline as the
|
|
@@ -27,33 +32,60 @@ const ACCEPTED_EVIDENCE_MAJORS = ["sanning.evidence/v1"];
|
|
|
27
32
|
// body_type is not a hard failure of the WRAPPER (the signature + body_hash
|
|
28
33
|
// still verify), but its body cannot be re-derived, so the per-body verdict is
|
|
29
34
|
// "undetermined" rather than "verified".
|
|
30
|
-
|
|
35
|
+
//
|
|
36
|
+
// The producer-to-agent cut (W-3, DL-73) renamed `record_bytes` to
|
|
37
|
+
// `event_bytes` and moved every body that carries it to a new major. This
|
|
38
|
+
// kernel reads ONLY those majors: `sanning.anchor.trace/v1` and `/v2`,
|
|
39
|
+
// `sanning.stamp.evidence/v1` and `sanning.evidence.export/v1` are unknown body
|
|
40
|
+
// types here and answer `partial`. DL-73 releases evidence made before the cut
|
|
41
|
+
// from any obligation to keep verifying, so there is no second field name to
|
|
42
|
+
// read and no dual-accept path.
|
|
43
|
+
export const ANCHOR_TRACE_BODY_TYPE = "sanning.anchor.trace/v3";
|
|
31
44
|
/**
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* defend (DL-30). Nothing new emits it.
|
|
45
|
+
* The name `event_bytes` replaced. Under the new majors a body entry that still
|
|
46
|
+
* carries it is a hard reject, never an ignored unknown field: ignored, it
|
|
47
|
+
* would read as a withheld record and the event would verify with nothing
|
|
48
|
+
* checked, the fail-open the new majors exist to prevent (governance.md §8,
|
|
49
|
+
* W-3).
|
|
38
50
|
*/
|
|
39
|
-
export const
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
export const LEGACY_EVENT_BYTES_FIELD = "record_bytes";
|
|
52
|
+
/**
|
|
53
|
+
* Name an envelope the accept-set refused. Without this a bundle whose
|
|
54
|
+
* envelopes carry a retired profile major (a half-migrated producer still
|
|
55
|
+
* writing `sanning.events/v1`) fails with no reason at all. Spelled with
|
|
56
|
+
* `JSON.stringify` so both kernels write the same sentence.
|
|
57
|
+
*/
|
|
58
|
+
function specVersionFinding(what, envelope, res) {
|
|
59
|
+
if (!isJsonObject(envelope) || res.specVersionOk)
|
|
60
|
+
return null;
|
|
61
|
+
if (!Object.prototype.hasOwnProperty.call(envelope, "spec_version")) {
|
|
62
|
+
return `${what} envelope has no spec_version`;
|
|
63
|
+
}
|
|
64
|
+
const sv = envelope.spec_version;
|
|
65
|
+
return `${what} envelope spec_version ${JSON.stringify(sv)} is not accepted by this kernel`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The reject for an entry that still carries `record_bytes`. One sentence,
|
|
69
|
+
* spelled the same in both kernels, so a negative vector can assert it.
|
|
70
|
+
*/
|
|
71
|
+
function legacyEventBytesError(entry, what, bodyType) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(entry, LEGACY_EVENT_BYTES_FIELD))
|
|
73
|
+
return null;
|
|
74
|
+
return (`${what} carries ${LEGACY_EVENT_BYTES_FIELD} in a ${bodyType} body ` +
|
|
75
|
+
"(renamed event_bytes) -- one bundle speaks one vocabulary");
|
|
76
|
+
}
|
|
45
77
|
// The issuer-composed attested-evidence-export body (specs/evidence-export.md
|
|
46
78
|
// §2). The wrapper is unchanged (`sanning.evidence/v1`, Ed25519-signed); this body
|
|
47
79
|
// carries a cached verdict + the inline source bundle + embedded RSA-PSS
|
|
48
80
|
// operator attestations. verifyEvidenceBundle dispatches on it to
|
|
49
81
|
// verifyExportBody (§5).
|
|
50
|
-
export const EXPORT_BODY_TYPE = "sanning.evidence.export/
|
|
82
|
+
export const EXPORT_BODY_TYPE = "sanning.evidence.export/v2";
|
|
51
83
|
/**
|
|
52
84
|
* The Sanning-signed interval stamp body (evidence-bundle.md §5.2, DL-58).
|
|
53
85
|
*
|
|
54
86
|
* Its own `body_type` rather than a widened anchor-trace body, and the reason
|
|
55
87
|
* is the whole decision: an anchor-trace checkpoint carries a PRODUCER-signed
|
|
56
|
-
* `sanning.events/
|
|
88
|
+
* `sanning.events/v2` envelope, and a stamp has no producer signature at all:
|
|
57
89
|
* Sanning places it, under DL-52, for a customer who holds no chain identity.
|
|
58
90
|
* The two rejected options were making `envelope` optional and synthesising an
|
|
59
91
|
* envelope that Sanning signs; the second would have made a Sanning
|
|
@@ -61,7 +93,7 @@ export const EXPORT_BODY_TYPE = "sanning.evidence.export/v1";
|
|
|
61
93
|
* unrecoverable once witnessed.
|
|
62
94
|
*/
|
|
63
95
|
export const STAMP_TRACE_BODY_TYPE = "sanning.stamp.trace/v1";
|
|
64
|
-
export const STAMP_EVIDENCE_BODY_TYPE = "sanning.stamp.evidence/
|
|
96
|
+
export const STAMP_EVIDENCE_BODY_TYPE = "sanning.stamp.evidence/v2";
|
|
65
97
|
/**
|
|
66
98
|
* The version string INSIDE a witnessed stamp body — a different string from
|
|
67
99
|
* the `body_type` above, and the two are easy to confuse.
|
|
@@ -86,12 +118,23 @@ function isEvidenceMajorAccepted(specVersion) {
|
|
|
86
118
|
return minor.length > 0 && /^[0-9]+$/.test(minor);
|
|
87
119
|
});
|
|
88
120
|
}
|
|
89
|
-
|
|
121
|
+
/**
|
|
122
|
+
* A bundle this kernel cannot read.
|
|
123
|
+
*
|
|
124
|
+
* `wrapperOk` is true only where the wrapper signature and `body_hash` were
|
|
125
|
+
* CHECKED and held before the body was found unreadable. It is then reported
|
|
126
|
+
* as held, because it did: `signatureOk` and `bodyHashOk` are statements about
|
|
127
|
+
* the wrapper, and a body this kernel cannot parse does not unsign it. The
|
|
128
|
+
* case reader decides whose pack a bundle is on exactly those two fields
|
|
129
|
+
* (`evidence-bundle.md` 5.5.1), so reporting `false` here let a holder who
|
|
130
|
+
* broke its own body shape read as a forger.
|
|
131
|
+
*/
|
|
132
|
+
function malformed(error, specVersionOk = false, wrapperOk = false) {
|
|
90
133
|
return {
|
|
91
134
|
status: "malformed",
|
|
92
135
|
specVersionOk,
|
|
93
|
-
bodyHashOk:
|
|
94
|
-
signatureOk:
|
|
136
|
+
bodyHashOk: wrapperOk,
|
|
137
|
+
signatureOk: wrapperOk,
|
|
95
138
|
bodyType: null,
|
|
96
139
|
onChainChecked: false,
|
|
97
140
|
checkpoints: [],
|
|
@@ -100,8 +143,77 @@ function malformed(error, specVersionOk = false) {
|
|
|
100
143
|
errors: [error],
|
|
101
144
|
};
|
|
102
145
|
}
|
|
103
|
-
|
|
104
|
-
|
|
146
|
+
/**
|
|
147
|
+
* The prefixes of the exception text this kernel writes ITSELF, in the words
|
|
148
|
+
* the Python kernel writes too. Anything outside this set comes from the host:
|
|
149
|
+
* a JSON parser, a codec, a crypto library. See `thrownDetail`.
|
|
150
|
+
*/
|
|
151
|
+
const KERNEL_WORDING = ["hexToBytes: "];
|
|
152
|
+
/**
|
|
153
|
+
* The detail a finding may append about a caught exception.
|
|
154
|
+
*
|
|
155
|
+
* THE RULE: a finding carries the text of a caught exception only where this
|
|
156
|
+
* kernel wrote that text. A finding travels into a case reading through `case`
|
|
157
|
+
* `reasons`, and both kernels promise one reading and one rendered sentence for
|
|
158
|
+
* one set of bytes; a host JSON parser says `Unexpected token 'h', "hello" is
|
|
159
|
+
* not valid JSON` here and `Expecting value: line 1 column 1 (char 0)` there,
|
|
160
|
+
* for one pack anyone can build (gate 4 on proof#140, finding 1).
|
|
161
|
+
*
|
|
162
|
+
* Returns `": <text>"` for this kernel's own wording and the empty string for
|
|
163
|
+
* anything else. The label at the call site already names the check that
|
|
164
|
+
* failed and both kernels agree on it character for character; what is dropped
|
|
165
|
+
* is the host's parse offset, which no reader can act on and which was never
|
|
166
|
+
* the same in the two kernels.
|
|
167
|
+
*/
|
|
168
|
+
function thrownDetail(e) {
|
|
169
|
+
const text = e instanceof Error ? e.message : String(e);
|
|
170
|
+
return KERNEL_WORDING.some((w) => text.startsWith(w)) ? `: ${text}` : "";
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Decode `bytes` as UTF-8 the way every other kernel does: a byte sequence that
|
|
174
|
+
* is not valid UTF-8 THROWS, and a leading byte-order mark is kept rather than
|
|
175
|
+
* eaten.
|
|
176
|
+
*
|
|
177
|
+
* ONE CALLER LEFT, and it is not a parse. `jcsParse` (`verifier.ts`) reads
|
|
178
|
+
* every set of signed bytes in this kernel now; this function survives only for
|
|
179
|
+
* the canonicality comparison on the stamp path, which needs the DECODED TEXT
|
|
180
|
+
* of bytes `jcsParse` has already accepted, to compare against `jcs(body)`. It
|
|
181
|
+
* performs exactly the decode `jcsParse` performs, so the comparison cannot
|
|
182
|
+
* read the bytes differently from the parse that produced `body`.
|
|
183
|
+
*
|
|
184
|
+
* `new TextDecoder()` alone is neither strict nor BOM-preserving: it replaces
|
|
185
|
+
* every invalid sequence with U+FFFD and strips a BOM. A stamp body carrying
|
|
186
|
+
* one raw `0x80` inside a string value read `verified` here and `failed` there
|
|
187
|
+
* because of it (gate 4 on proof#140). The symmetric claim the first version of
|
|
188
|
+
* this comment made, that "Python's `json.loads` raises on both", is FALSE and
|
|
189
|
+
* is what hid gate 5's defect for a round: `json.loads(<bytes>)` strips a BOM
|
|
190
|
+
* too. Python's strict reader is `jcs_loads`, and the rule both kernels hold to
|
|
191
|
+
* is stated once, in `jcsParse`.
|
|
192
|
+
*/
|
|
193
|
+
function decodeUtf8Strict(bytes) {
|
|
194
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Name an envelope `public_key` or `signature` spelled outside the hex charset
|
|
198
|
+
* or at the wrong length (proof#142). This kernel's envelope layer already
|
|
199
|
+
* fails such an envelope, and the Python one's decoder accepts ASCII
|
|
200
|
+
* whitespace (proof#135), so both bundle verifiers name it here, in the same
|
|
201
|
+
* words, and the two kernels give one reading. Only a STRING is checked: a
|
|
202
|
+
* missing or non-string field is the envelope layer's to fail, and it does.
|
|
203
|
+
*/
|
|
204
|
+
function envelopeHexError(what, envelope) {
|
|
205
|
+
if (!isJsonObject(envelope))
|
|
206
|
+
return null;
|
|
207
|
+
for (const [name, nibbles] of [
|
|
208
|
+
["public_key", 64],
|
|
209
|
+
["signature", 128],
|
|
210
|
+
]) {
|
|
211
|
+
const value = envelope[name];
|
|
212
|
+
if (typeof value === "string" && !isHexField(value, nibbles)) {
|
|
213
|
+
return `${what} envelope ${name} is not ${nibbles} hex characters`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
105
217
|
}
|
|
106
218
|
/**
|
|
107
219
|
* Spell a value inside a finding the way the Python kernel spells it.
|
|
@@ -116,13 +228,18 @@ function stringifyErr(e) {
|
|
|
116
228
|
function jsonStr(value) {
|
|
117
229
|
return JSON.stringify(value === undefined ? null : value) ?? "null";
|
|
118
230
|
}
|
|
119
|
-
// Verify a `sanning.evidence/v1` bundle
|
|
231
|
+
// Verify a `sanning.evidence/v1` bundle and dispatch on its `body_type`.
|
|
120
232
|
//
|
|
121
233
|
// Returns a structured per-checkpoint + per-event result and a recomputed
|
|
122
234
|
// rollup `status`. The producer's asserted `verdict.status` is surfaced
|
|
123
235
|
// (`assertedStatus`) but NEVER trusted — the displayed status is the one this
|
|
124
236
|
// function recomputes (evidence-bundle §1 principle 3 / §4 step 4).
|
|
125
237
|
export async function verifyEvidenceBundle(bundle, options = {}) {
|
|
238
|
+
return verifyBundle(bundle, options, false);
|
|
239
|
+
}
|
|
240
|
+
// `exportSource`: read the bundle as an export's source (evidence-export.md
|
|
241
|
+
// §2.4), where the only recognised body is `sanning.anchor.trace/v3`.
|
|
242
|
+
async function verifyBundle(bundle, options, exportSource) {
|
|
126
243
|
// --- Step 1: parse + reject unknown spec_version major ---------------------
|
|
127
244
|
if (bundle === null || typeof bundle !== "object" || Array.isArray(bundle)) {
|
|
128
245
|
return malformed("evidence bundle is not a JSON object");
|
|
@@ -157,26 +274,47 @@ export async function verifyEvidenceBundle(bundle, options = {}) {
|
|
|
157
274
|
}
|
|
158
275
|
// --- Step 2: verify the wrapper signature + recompute body_hash ------------
|
|
159
276
|
const errors = [];
|
|
277
|
+
// Every wrapper hex field is read STRICTLY, before any decoder sees it
|
|
278
|
+
// (proof#142). The Python decoder skipped ASCII whitespace, and the wrapper
|
|
279
|
+
// `signature` sits outside the signed scope, so one space in it, added by
|
|
280
|
+
// anyone and with no key, made a pack verify there and fail here. The same
|
|
281
|
+
// named failure in both kernels, for each field: `public_key` and
|
|
282
|
+
// `signature` are 64 and 128 hex characters in either case (proof#135,
|
|
283
|
+
// option B), and `body_hash` is 64 LOWER-case hex characters, the form it is
|
|
284
|
+
// recomputed in and compared against.
|
|
160
285
|
let bodyHashOk = false;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
bodyHashOk = recomputed === b.body_hash;
|
|
164
|
-
if (!bodyHashOk) {
|
|
165
|
-
errors.push(`body_hash mismatch: bundle=${b.body_hash} recomputed=${recomputed}`);
|
|
166
|
-
}
|
|
286
|
+
if (!isHexField(b.body_hash, 64, true)) {
|
|
287
|
+
errors.push("wrapper body_hash is not 64 lowercase hex characters");
|
|
167
288
|
}
|
|
168
|
-
|
|
169
|
-
|
|
289
|
+
else {
|
|
290
|
+
try {
|
|
291
|
+
const recomputed = await sha256Hex(utf8(jcs(b.body)));
|
|
292
|
+
bodyHashOk = recomputed === b.body_hash;
|
|
293
|
+
if (!bodyHashOk) {
|
|
294
|
+
errors.push(`body_hash mismatch: bundle=${b.body_hash} recomputed=${recomputed}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
errors.push(`body canonicalization failed${thrownDetail(e)}`);
|
|
299
|
+
}
|
|
170
300
|
}
|
|
171
301
|
let signatureOk = false;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
302
|
+
const keyFormOk = isHexField(b.public_key, 64);
|
|
303
|
+
const signatureFormOk = isHexField(b.signature, 128);
|
|
304
|
+
if (!keyFormOk)
|
|
305
|
+
errors.push("wrapper public_key is not 64 hex characters");
|
|
306
|
+
if (!signatureFormOk)
|
|
307
|
+
errors.push("wrapper signature is not 128 hex characters");
|
|
308
|
+
if (keyFormOk && signatureFormOk) {
|
|
309
|
+
try {
|
|
310
|
+
const { signature: _sig, co_signatures: _co, ...bundleForSig } = b;
|
|
311
|
+
signatureOk = await ed25519Verify(b.signature, utf8(jcs(bundleForSig)), b.public_key);
|
|
312
|
+
if (!signatureOk)
|
|
313
|
+
errors.push("wrapper Ed25519 signature verification failed");
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
errors.push(`wrapper signature verification error${thrownDetail(e)}`);
|
|
317
|
+
}
|
|
180
318
|
}
|
|
181
319
|
const assertedStatus = typeof b.verdict?.status === "string" ? b.verdict.status : null;
|
|
182
320
|
// A broken wrapper (bad signature or tampered body) is a hard failure — the
|
|
@@ -197,6 +335,25 @@ export async function verifyEvidenceBundle(bundle, options = {}) {
|
|
|
197
335
|
}
|
|
198
336
|
// --- Body dispatch ---------------------------------------------------------
|
|
199
337
|
const bodyType = typeof b.body_type === "string" ? b.body_type : null;
|
|
338
|
+
// Read as an export's source (evidence-export.md §2.4), the only body this
|
|
339
|
+
// kernel recognises is sanning.anchor.trace/v3. Any other body, even one it
|
|
340
|
+
// verifies on its own, is undetermined here, after the wrapper checks above.
|
|
341
|
+
if (exportSource && bodyType !== ANCHOR_TRACE_BODY_TYPE) {
|
|
342
|
+
errors.push(`body_type ${JSON.stringify(bodyType)} is not an export source ` +
|
|
343
|
+
`(only ${ANCHOR_TRACE_BODY_TYPE}); wrapper verified, body undetermined`);
|
|
344
|
+
return {
|
|
345
|
+
status: "partial",
|
|
346
|
+
specVersionOk,
|
|
347
|
+
bodyHashOk,
|
|
348
|
+
signatureOk,
|
|
349
|
+
bodyType,
|
|
350
|
+
onChainChecked: false,
|
|
351
|
+
checkpoints: [],
|
|
352
|
+
events: [],
|
|
353
|
+
assertedStatus,
|
|
354
|
+
errors,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
200
357
|
// Attested-evidence-export body (evidence-export.md §5). The wrapper is
|
|
201
358
|
// authentic (Ed25519 issuer signature + body_hash verified above, §5 step 2);
|
|
202
359
|
// hand off to the export verifier for steps 3–9.
|
|
@@ -213,12 +370,12 @@ export async function verifyEvidenceBundle(bundle, options = {}) {
|
|
|
213
370
|
if (bodyType === STAMP_EVIDENCE_BODY_TYPE) {
|
|
214
371
|
return verifyStampEvidence(b, specVersionOk, assertedStatus, errors, options);
|
|
215
372
|
}
|
|
216
|
-
if (bodyType !== ANCHOR_TRACE_BODY_TYPE
|
|
373
|
+
if (bodyType !== ANCHOR_TRACE_BODY_TYPE) {
|
|
217
374
|
// The wrapper is authentic but we don't know this body's structure. Per
|
|
218
375
|
// wrap-don't-merge, that is "partial" (cryptographically sound wrapper,
|
|
219
376
|
// body verdict undetermined) — not a failure.
|
|
220
377
|
errors.push(`body_type ${JSON.stringify(bodyType)} is not verifiable by this kernel ` +
|
|
221
|
-
`(only ${
|
|
378
|
+
`(only ${ANCHOR_TRACE_BODY_TYPE}); wrapper verified, body undetermined`);
|
|
222
379
|
return {
|
|
223
380
|
status: "partial",
|
|
224
381
|
specVersionOk,
|
|
@@ -232,99 +389,101 @@ export async function verifyEvidenceBundle(bundle, options = {}) {
|
|
|
232
389
|
errors,
|
|
233
390
|
};
|
|
234
391
|
}
|
|
235
|
-
return verifyAnchorTrace(b,
|
|
392
|
+
return verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, options);
|
|
236
393
|
}
|
|
237
394
|
/**
|
|
238
|
-
*
|
|
395
|
+
* Recompute an export's source verdict (evidence-export.md §5 step 4).
|
|
239
396
|
*
|
|
240
|
-
* The
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
397
|
+
* The only export source is `sanning.anchor.trace/v3` (§2.2, §2.4). Any other
|
|
398
|
+
* inline source body is unrecognised *as a source*, even one this kernel
|
|
399
|
+
* verifies on its own, such as a `sanning.stamp.evidence/v2` pack: §2.4 makes
|
|
400
|
+
* its source verdict undetermined, so the export cannot reach `verified`. The
|
|
401
|
+
* source wrapper is still checked first, because a forged source is a failure
|
|
402
|
+
* whatever its body.
|
|
403
|
+
*/
|
|
404
|
+
async function verifyExportSource(source, options) {
|
|
405
|
+
return verifyBundle(source, options, true);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Index a source bundle's checkpoints by the key its attestations use: the
|
|
409
|
+
* derived `checkpoint_ref`. Only a `sanning.anchor.trace/v3` source has
|
|
410
|
+
* checkpoints an attestation can resolve (evidence-export.md §2.4).
|
|
245
411
|
*/
|
|
246
412
|
async function indexSourceCheckpoints(bundle) {
|
|
247
413
|
const out = new Map();
|
|
414
|
+
if (!isJsonObject(bundle) || bundle.body_type !== ANCHOR_TRACE_BODY_TYPE)
|
|
415
|
+
return out;
|
|
248
416
|
const sb = bundle.body;
|
|
249
|
-
const isV1 = bundle.body_type === ANCHOR_TRACE_BODY_TYPE_V1;
|
|
250
417
|
if (!sb || typeof sb !== "object" || !Array.isArray(sb.checkpoints))
|
|
251
418
|
return out;
|
|
252
419
|
for (const cp of sb.checkpoints) {
|
|
253
420
|
if (!cp || typeof cp !== "object")
|
|
254
421
|
continue;
|
|
255
|
-
let
|
|
422
|
+
let eventBytes;
|
|
256
423
|
try {
|
|
257
|
-
if (typeof cp.
|
|
258
|
-
|
|
424
|
+
if (typeof cp.event_bytes === "string")
|
|
425
|
+
eventBytes = hexToBytes(cp.event_bytes);
|
|
259
426
|
}
|
|
260
427
|
catch {
|
|
261
|
-
|
|
428
|
+
eventBytes = undefined;
|
|
262
429
|
}
|
|
263
|
-
const keyed = await resolveCheckpointKey(cp,
|
|
430
|
+
const keyed = await resolveCheckpointKey(cp, eventBytes);
|
|
264
431
|
if (keyed.key !== null && !out.has(keyed.key))
|
|
265
432
|
out.set(keyed.key, cp);
|
|
266
433
|
}
|
|
267
434
|
return out;
|
|
268
435
|
}
|
|
269
436
|
/**
|
|
270
|
-
* Resolve a checkpoint's join key and its witness reference
|
|
271
|
-
* body form we are reading.
|
|
437
|
+
* Resolve a checkpoint's join key and its witness reference.
|
|
272
438
|
*
|
|
273
|
-
* ──
|
|
439
|
+
* ── Why a retired spelling is a reject rather than a preference ──
|
|
274
440
|
*
|
|
275
|
-
* A checkpoint carrying
|
|
276
|
-
* inside one signature. A reader that silently prefers
|
|
277
|
-
* answer from a reader that prefers the other, over the
|
|
278
|
-
* and both look like they verified. So
|
|
279
|
-
*
|
|
280
|
-
* resolved. This is cheap to enforce here and impossible to enforce later.
|
|
441
|
+
* A checkpoint carrying a retired spelling beside the current one is two
|
|
442
|
+
* answers to one question inside one signature. A reader that silently prefers
|
|
443
|
+
* one gives a different answer from a reader that prefers the other, over the
|
|
444
|
+
* same signed bytes, and both look like they verified. So `tx_id` and
|
|
445
|
+
* `record_bytes` are rejected rather than resolved.
|
|
281
446
|
*
|
|
282
|
-
* ── Why
|
|
447
|
+
* ── Why the key is recomputed ──
|
|
283
448
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
* event pointing at it left a bundle that still verified. In v2 the key IS the
|
|
287
|
-
* bytes, so the same relabel fails here. v1's behaviour is preserved exactly
|
|
288
|
-
* as it was — deliberately, because those bundles are already signed and
|
|
289
|
-
* re-interpreting them is not backwards compatibility.
|
|
449
|
+
* The key IS the bytes: relabelling a checkpoint and every event pointing at it
|
|
450
|
+
* fails here, because nothing is taken from the label.
|
|
290
451
|
*/
|
|
291
|
-
async function resolveCheckpointKey(cp,
|
|
452
|
+
async function resolveCheckpointKey(cp, eventBytes) {
|
|
292
453
|
const errors = [];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"one bundle speaks one vocabulary");
|
|
297
|
-
return { key: null, anchorRef: null, errors };
|
|
298
|
-
}
|
|
299
|
-
if (typeof cp.tx_id !== "string" || cp.tx_id === "") {
|
|
300
|
-
errors.push("checkpoint missing tx_id");
|
|
301
|
-
return { key: null, anchorRef: null, errors };
|
|
302
|
-
}
|
|
303
|
-
// v1's tx_id IS an Arweave transaction — its own spec said so. Surfacing it
|
|
304
|
-
// through the same shape as v2 gives ONE reader-side vocabulary without
|
|
305
|
-
// touching a byte of the signed bundle.
|
|
306
|
-
return { key: cp.tx_id, anchorRef: { kind: "arweave", id: cp.tx_id }, errors };
|
|
307
|
-
}
|
|
454
|
+
// Every early return below leaves before `anchor_ref` is ever looked at, so
|
|
455
|
+
// nothing there is "malformed": it is unexamined, which is not the same claim.
|
|
456
|
+
const unexamined = { key: null, anchorRef: null, anchorRefMalformed: false, errors };
|
|
308
457
|
if (cp.tx_id !== undefined) {
|
|
309
|
-
errors.push(
|
|
310
|
-
return
|
|
458
|
+
errors.push(`${ANCHOR_TRACE_BODY_TYPE} checkpoint carries legacy tx_id -- one bundle speaks one vocabulary`);
|
|
459
|
+
return unexamined;
|
|
460
|
+
}
|
|
461
|
+
const legacy = legacyEventBytesError(cp, "checkpoint", ANCHOR_TRACE_BODY_TYPE);
|
|
462
|
+
if (legacy !== null) {
|
|
463
|
+
errors.push(legacy);
|
|
464
|
+
return unexamined;
|
|
311
465
|
}
|
|
312
466
|
if (typeof cp.checkpoint_ref !== "string" || cp.checkpoint_ref === "") {
|
|
313
467
|
errors.push("checkpoint missing checkpoint_ref");
|
|
314
|
-
return
|
|
468
|
+
return unexamined;
|
|
315
469
|
}
|
|
316
|
-
if (
|
|
470
|
+
if (eventBytes === undefined) {
|
|
317
471
|
// Without the bytes the key cannot be recomputed, so it cannot be trusted.
|
|
318
|
-
errors.push("checkpoint_ref cannot be verified --
|
|
319
|
-
return
|
|
472
|
+
errors.push("checkpoint_ref cannot be verified -- event_bytes missing or not hex");
|
|
473
|
+
return unexamined;
|
|
320
474
|
}
|
|
321
|
-
const derived = await sha256Hex(
|
|
475
|
+
const derived = await sha256Hex(eventBytes);
|
|
322
476
|
if (derived !== cp.checkpoint_ref.toLowerCase()) {
|
|
323
|
-
errors.push(`checkpoint_ref does not match SHA-256(
|
|
324
|
-
return
|
|
477
|
+
errors.push(`checkpoint_ref does not match SHA-256(event_bytes): claimed=${cp.checkpoint_ref} derived=${derived}`);
|
|
478
|
+
return unexamined;
|
|
325
479
|
}
|
|
326
|
-
const
|
|
327
|
-
return {
|
|
480
|
+
const parsed = parseAnchorRef(cp.anchor_ref, errors);
|
|
481
|
+
return {
|
|
482
|
+
key: derived,
|
|
483
|
+
anchorRef: parsed.ref,
|
|
484
|
+
anchorRefMalformed: parsed.malformed,
|
|
485
|
+
errors,
|
|
486
|
+
};
|
|
328
487
|
}
|
|
329
488
|
/**
|
|
330
489
|
* Validate an optional `anchor_ref` (§5.1.1).
|
|
@@ -343,39 +502,54 @@ async function resolveCheckpointKey(cp, recordBytes, isV1) {
|
|
|
343
502
|
* So this validates the SHAPE and never the vocabulary. A malformed
|
|
344
503
|
* `anchor_ref` is a defect in the bundle; an unknown `kind` is a defect in
|
|
345
504
|
* the reader, and only the first is the bundle's fault.
|
|
505
|
+
*
|
|
506
|
+
* ── Absent and malformed are not the same null (proof#120) ──
|
|
507
|
+
*
|
|
508
|
+
* Both leave no `AnchorRef` to re-fetch, and both used to be reported the same
|
|
509
|
+
* way downstream: an all-`unreachable` on-chain result, although no gateway was
|
|
510
|
+
* asked in either case. For the ABSENT case that is at least the note
|
|
511
|
+
* `evidence-bundle.md` §5.1.1 reserves for "a checkpoint with no `anchor_ref`".
|
|
512
|
+
* For the MALFORMED case it is a manufactured outage: the bundle named a
|
|
513
|
+
* witness, the name was unreadable, and the reader was told the public record
|
|
514
|
+
* could not be reached. So the two come back as different facts, and `malformed`
|
|
515
|
+
* carries the difference.
|
|
346
516
|
*/
|
|
347
517
|
function parseAnchorRef(raw, errors) {
|
|
348
|
-
if (raw === undefined || raw === null)
|
|
349
|
-
return null; // placed, not yet witnessed
|
|
518
|
+
if (raw === undefined || raw === null) {
|
|
519
|
+
return { ref: null, malformed: false }; // placed, not yet witnessed
|
|
520
|
+
}
|
|
350
521
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
351
522
|
errors.push("anchor_ref must be an object");
|
|
352
|
-
return null;
|
|
523
|
+
return { ref: null, malformed: true };
|
|
353
524
|
}
|
|
354
525
|
const o = raw;
|
|
355
526
|
if (typeof o.kind !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(o.kind)) {
|
|
356
527
|
errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind)}`);
|
|
357
|
-
return null;
|
|
528
|
+
return { ref: null, malformed: true };
|
|
358
529
|
}
|
|
359
530
|
if (typeof o.id !== "string" || o.id === "") {
|
|
360
531
|
errors.push("anchor_ref.id is missing or empty");
|
|
361
|
-
return null;
|
|
532
|
+
return { ref: null, malformed: true };
|
|
362
533
|
}
|
|
363
534
|
const ref = o.ref;
|
|
364
535
|
if (ref !== undefined && (typeof ref !== "object" || ref === null || Array.isArray(ref))) {
|
|
365
536
|
errors.push("anchor_ref.ref must be an object when present");
|
|
366
|
-
return null;
|
|
537
|
+
return { ref: null, malformed: true };
|
|
367
538
|
}
|
|
368
539
|
return {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
540
|
+
ref: {
|
|
541
|
+
kind: o.kind,
|
|
542
|
+
id: o.id,
|
|
543
|
+
...(ref === undefined ? {} : { ref: ref }),
|
|
544
|
+
},
|
|
545
|
+
malformed: false,
|
|
372
546
|
};
|
|
373
547
|
}
|
|
374
|
-
async function verifyAnchorTrace(b,
|
|
548
|
+
async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, options) {
|
|
375
549
|
const body = b.body;
|
|
376
|
-
const
|
|
550
|
+
const bodyType = ANCHOR_TRACE_BODY_TYPE;
|
|
377
551
|
if (!Array.isArray(body.checkpoints) || !Array.isArray(body.events)) {
|
|
378
|
-
return malformed(`${bodyType} body must have checkpoints[] and events[]`, true);
|
|
552
|
+
return malformed(`${bodyType} body must have checkpoints[] and events[]`, true, true);
|
|
379
553
|
}
|
|
380
554
|
const gateways = options.gateways ?? [];
|
|
381
555
|
const onChainChecked = gateways.length > 0;
|
|
@@ -390,6 +564,11 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
390
564
|
const byKey = new Map();
|
|
391
565
|
const anchorByKey = new Map();
|
|
392
566
|
const checkpointResults = [];
|
|
567
|
+
// proof#120. Kept beside the results rather than on them: "the bundle named
|
|
568
|
+
// a witness we could not read" is an input defect the checkpoint's own
|
|
569
|
+
// `errors` already states, not a new dimension of the published verdict, and
|
|
570
|
+
// `CheckpointResult` is a shape downstream surfaces render.
|
|
571
|
+
const malformedAnchorRef = new Set();
|
|
393
572
|
for (const rawCp of body.checkpoints) {
|
|
394
573
|
// `checkpoints[]` is attacker-shaped, and a verifier must never raise on
|
|
395
574
|
// adversarial input: a null entry used to reach `cp.tx_id` and hand the
|
|
@@ -416,17 +595,17 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
416
595
|
const cpErrors = [];
|
|
417
596
|
let envelopeOk = false;
|
|
418
597
|
let merkleRootOk = false;
|
|
419
|
-
// Decode
|
|
598
|
+
// Decode event_bytes first: in v2 the join key is DERIVED from it, so it
|
|
420
599
|
// is an input to identity, not just to the payload binding below.
|
|
421
|
-
let
|
|
600
|
+
let eventBytes;
|
|
422
601
|
try {
|
|
423
|
-
if (typeof cp.
|
|
424
|
-
|
|
602
|
+
if (typeof cp.event_bytes === "string")
|
|
603
|
+
eventBytes = hexToBytes(cp.event_bytes);
|
|
425
604
|
}
|
|
426
605
|
catch (e) {
|
|
427
|
-
cpErrors.push(`checkpoint
|
|
606
|
+
cpErrors.push(`checkpoint event_bytes is not hex${thrownDetail(e)}`);
|
|
428
607
|
}
|
|
429
|
-
const keyed = await resolveCheckpointKey(cp,
|
|
608
|
+
const keyed = await resolveCheckpointKey(cp, eventBytes);
|
|
430
609
|
cpErrors.push(...keyed.errors);
|
|
431
610
|
// A duplicate key is a HARD failure, not a note. `byKey` below keeps the
|
|
432
611
|
// FIRST occurrence, so a second checkpoint claiming the same key is
|
|
@@ -440,7 +619,7 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
440
619
|
let duplicateKey = false;
|
|
441
620
|
if (keyed.key !== null) {
|
|
442
621
|
if (byKey.has(keyed.key)) {
|
|
443
|
-
cpErrors.push(`duplicate checkpoint
|
|
622
|
+
cpErrors.push(`duplicate checkpoint checkpoint_ref ${keyed.key}`);
|
|
444
623
|
duplicateKey = true;
|
|
445
624
|
}
|
|
446
625
|
else {
|
|
@@ -451,16 +630,20 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
451
630
|
// Verify the checkpoint envelope against its committed record (external
|
|
452
631
|
// commitment — the record is the payload these external envelopes bind to).
|
|
453
632
|
try {
|
|
454
|
-
const res = await verifyEnvelope(cp.envelope,
|
|
633
|
+
const res = await verifyEnvelope(cp.envelope, eventBytes !== undefined ? { payloadBytes: eventBytes } : {});
|
|
455
634
|
// A checkpoint must fully bind: signature + the committed record matching
|
|
456
635
|
// payload_hash. An undetermined binding (no record) is not enough to
|
|
457
636
|
// anchor the events' inclusion claims to a trustworthy root.
|
|
458
|
-
|
|
637
|
+
const hexError = envelopeHexError("checkpoint", cp.envelope);
|
|
638
|
+
envelopeOk = res.ok && res.payloadHashOk === true && hexError === null;
|
|
459
639
|
// See the event loop below: a degenerate envelope is a malformed input,
|
|
460
640
|
// not a failed check (proof#76).
|
|
461
641
|
if (!isJsonObject(cp.envelope)) {
|
|
462
642
|
cpErrors.push("checkpoint envelope is not a JSON object");
|
|
463
643
|
}
|
|
644
|
+
else if (hexError !== null) {
|
|
645
|
+
cpErrors.push(hexError);
|
|
646
|
+
}
|
|
464
647
|
else if (!res.signatureOk) {
|
|
465
648
|
cpErrors.push("checkpoint envelope signature failed");
|
|
466
649
|
}
|
|
@@ -471,25 +654,28 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
471
654
|
for (const m of res.errors)
|
|
472
655
|
if (m.startsWith("malformed "))
|
|
473
656
|
cpErrors.push(`checkpoint envelope ${m}`);
|
|
657
|
+
const specFinding = specVersionFinding("checkpoint", cp.envelope, res);
|
|
658
|
+
if (specFinding !== null)
|
|
659
|
+
cpErrors.push(specFinding);
|
|
474
660
|
// WHY the binding is undetermined decides which sentence is true.
|
|
475
|
-
//
|
|
661
|
+
// event_bytes really absent is one thing; event_bytes present and
|
|
476
662
|
// unbindable (a degenerate envelope, unreadable hex) is another, and
|
|
477
663
|
// naming the wrong cause sends a reader to look for a field that is
|
|
478
664
|
// sitting right there (proof#76).
|
|
479
665
|
if (res.payloadHashOk === null) {
|
|
480
|
-
cpErrors.push(
|
|
481
|
-
? "checkpoint
|
|
666
|
+
cpErrors.push(eventBytes === undefined
|
|
667
|
+
? "checkpoint event_bytes missing -- root untrusted"
|
|
482
668
|
: "checkpoint record binding not checked: root untrusted");
|
|
483
669
|
}
|
|
484
670
|
}
|
|
485
671
|
catch (e) {
|
|
486
|
-
cpErrors.push(`checkpoint envelope verify error
|
|
672
|
+
cpErrors.push(`checkpoint envelope verify error${thrownDetail(e)}`);
|
|
487
673
|
}
|
|
488
674
|
// The committed record's merkle_root must equal the checkpoint's claimed
|
|
489
675
|
// merkle_root (recompute, don't trust the bundle's top-level claim).
|
|
490
|
-
if (
|
|
676
|
+
if (eventBytes !== undefined) {
|
|
491
677
|
try {
|
|
492
|
-
const record =
|
|
678
|
+
const record = jcsParse(eventBytes);
|
|
493
679
|
const committedRoot = record.event?.merkle_root;
|
|
494
680
|
merkleRootOk = typeof committedRoot === "string" && committedRoot === cp.merkle_root;
|
|
495
681
|
if (!merkleRootOk) {
|
|
@@ -498,7 +684,7 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
498
684
|
}
|
|
499
685
|
}
|
|
500
686
|
catch (e) {
|
|
501
|
-
cpErrors.push(`checkpoint record is not JSON
|
|
687
|
+
cpErrors.push(`checkpoint record is not JSON${thrownDetail(e)}`);
|
|
502
688
|
}
|
|
503
689
|
}
|
|
504
690
|
checkpointResults.push({
|
|
@@ -517,6 +703,9 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
517
703
|
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0 && !duplicateKey,
|
|
518
704
|
errors: cpErrors,
|
|
519
705
|
});
|
|
706
|
+
if (keyed.anchorRefMalformed) {
|
|
707
|
+
malformedAnchorRef.add(checkpointResults[checkpointResults.length - 1]);
|
|
708
|
+
}
|
|
520
709
|
}
|
|
521
710
|
// --- Step 4: per-event ------------------------------------------------------
|
|
522
711
|
const eventResults = [];
|
|
@@ -542,17 +731,41 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
542
731
|
const eventId = getEventId(ev.envelope);
|
|
543
732
|
let envelopeOk = false;
|
|
544
733
|
let payloadBindingOk = null;
|
|
545
|
-
|
|
734
|
+
// A retired field name under this major is a reject, never an ignored
|
|
735
|
+
// unknown field: ignored, it reads as a withheld record (W-3).
|
|
736
|
+
const legacyError = legacyEventBytesError(ev, "event", bodyType);
|
|
737
|
+
if (legacyError !== null)
|
|
738
|
+
evErrors.push(legacyError);
|
|
739
|
+
// MALFORMED IS A FAILURE; ABSENT IS UNDETERMINED (proof#85).
|
|
740
|
+
//
|
|
741
|
+
// `event_bytes` that will not decode leaves `eventBytes` undefined, and an
|
|
742
|
+
// undefined `eventBytes` is exactly how a WITHHELD record is spelled one
|
|
743
|
+
// line down. So an event carrying garbage in place of its record used to
|
|
744
|
+
// reach the same tri-state as an event carrying nothing: `payloadHashOk`
|
|
745
|
+
// null, `ok` true, a leading tick on the line, and a `verified` rollup over
|
|
746
|
+
// a record nobody could read. The only thing wrong with the pack was the
|
|
747
|
+
// bytes, and it was the one thing the verdict did not say.
|
|
748
|
+
//
|
|
749
|
+
// A record the producer chose not to disclose is the customer exercising
|
|
750
|
+
// minimum disclosure. A record that is present and unreadable is a defect
|
|
751
|
+
// in the artifact, and this kernel's standing rule (the same one
|
|
752
|
+
// `checkpoints[]` already applies through `keyed.errors`) is that a defect
|
|
753
|
+
// we can see fails.
|
|
754
|
+
let eventBytes;
|
|
755
|
+
let eventBytesMalformed = false;
|
|
546
756
|
try {
|
|
547
|
-
if (typeof ev.
|
|
548
|
-
|
|
757
|
+
if (typeof ev.event_bytes === "string")
|
|
758
|
+
eventBytes = hexToBytes(ev.event_bytes);
|
|
549
759
|
}
|
|
550
760
|
catch (e) {
|
|
551
|
-
|
|
761
|
+
eventBytesMalformed = true;
|
|
762
|
+
evErrors.push(`event event_bytes is not hex${thrownDetail(e)}`);
|
|
552
763
|
}
|
|
553
764
|
try {
|
|
554
|
-
const res = await verifyEnvelope(ev.envelope,
|
|
555
|
-
|
|
765
|
+
const res = await verifyEnvelope(ev.envelope, eventBytes !== undefined ? { payloadBytes: eventBytes } : {});
|
|
766
|
+
const hexError = envelopeHexError("event", ev.envelope);
|
|
767
|
+
// ok tolerates an undetermined binding (withheld record)
|
|
768
|
+
envelopeOk = res.ok && hexError === null;
|
|
556
769
|
payloadBindingOk = res.payloadHashOk;
|
|
557
770
|
// P-10: report what could not be determined, never a fault that was never
|
|
558
771
|
// observed. There is no envelope here, so no signature was checked and no
|
|
@@ -564,6 +777,9 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
564
777
|
if (!isJsonObject(ev.envelope)) {
|
|
565
778
|
evErrors.push("event envelope is not a JSON object");
|
|
566
779
|
}
|
|
780
|
+
else if (hexError !== null) {
|
|
781
|
+
evErrors.push(hexError);
|
|
782
|
+
}
|
|
567
783
|
else if (!res.signatureOk) {
|
|
568
784
|
evErrors.push("event envelope signature failed");
|
|
569
785
|
}
|
|
@@ -574,19 +790,21 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
574
790
|
for (const m of res.errors)
|
|
575
791
|
if (m.startsWith("malformed "))
|
|
576
792
|
evErrors.push(`event envelope ${m}`);
|
|
793
|
+
const specFinding = specVersionFinding("event", ev.envelope, res);
|
|
794
|
+
if (specFinding !== null)
|
|
795
|
+
evErrors.push(specFinding);
|
|
577
796
|
}
|
|
578
797
|
catch (e) {
|
|
579
|
-
evErrors.push(`event envelope verify error
|
|
798
|
+
evErrors.push(`event envelope verify error${thrownDetail(e)}`);
|
|
580
799
|
}
|
|
581
800
|
// Bind the event to exactly one checkpoint in the bundle, then verify
|
|
582
801
|
// RFC 9162 inclusion against THAT checkpoint's merkle_root.
|
|
583
802
|
const incl = ev.inclusion;
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
const wrongField = isV1 ? "checkpoint_ref" : "checkpoint_tx_id";
|
|
803
|
+
// An inclusion carrying the retired `checkpoint_tx_id` spelling is a mixed
|
|
804
|
+
// bundle, and resolving it anyway is how an event silently binds to a
|
|
805
|
+
// checkpoint nobody meant.
|
|
806
|
+
const keyField = "checkpoint_ref";
|
|
807
|
+
const wrongField = "checkpoint_tx_id";
|
|
590
808
|
const inclAsRecord = incl;
|
|
591
809
|
const rawKey = inclAsRecord ? inclAsRecord[keyField] : undefined;
|
|
592
810
|
const strayKey = inclAsRecord ? inclAsRecord[wrongField] : undefined;
|
|
@@ -631,11 +849,11 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
631
849
|
}
|
|
632
850
|
}
|
|
633
851
|
catch (e) {
|
|
634
|
-
evErrors.push(`inclusion verify error
|
|
852
|
+
evErrors.push(`inclusion verify error${thrownDetail(e)}`);
|
|
635
853
|
}
|
|
636
854
|
}
|
|
637
855
|
// Content (raw-log) binding — shared with every other event-carrying body.
|
|
638
|
-
const contentRes = await verifyDisclosedContent(ev, eventId,
|
|
856
|
+
const contentRes = await verifyDisclosedContent(ev, eventId, eventBytes, options.content);
|
|
639
857
|
const contentOk = contentRes.contentOk;
|
|
640
858
|
evErrors.push(...contentRes.errors);
|
|
641
859
|
eventResults.push({
|
|
@@ -650,13 +868,22 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
650
868
|
// present checkpoint, and its disclosed content did not MISMATCH. A
|
|
651
869
|
// withheld record or undisclosed content (binding/content null) does not
|
|
652
870
|
// fail it.
|
|
653
|
-
|
|
871
|
+
// `!eventBytesMalformed`: present-and-unreadable record bytes are a
|
|
872
|
+
// defect in the artifact, never a disclosure choice (proof#85).
|
|
873
|
+
ok: envelopeOk &&
|
|
874
|
+
inclusionOk &&
|
|
875
|
+
checkpointBound &&
|
|
876
|
+
contentOk !== false &&
|
|
877
|
+
legacyError === null &&
|
|
878
|
+
!eventBytesMalformed,
|
|
654
879
|
errors: evErrors,
|
|
655
880
|
});
|
|
656
881
|
}
|
|
657
882
|
// --- Step 5: optional on-chain re-fetch ------------------------------------
|
|
658
883
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
659
884
|
let gatewayUnavailable = false;
|
|
885
|
+
let uncheckedKind = false;
|
|
886
|
+
let uncheckedId = false;
|
|
660
887
|
if (onChainChecked) {
|
|
661
888
|
if (typeof fetchImpl !== "function") {
|
|
662
889
|
gatewayUnavailable = true;
|
|
@@ -673,6 +900,20 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
673
900
|
// the run to partial) and NEVER as a mismatch. Treating "we never
|
|
674
901
|
// anchored it yet" as "the chain disagrees" would be the same false
|
|
675
902
|
// accusation this whole change is removing.
|
|
903
|
+
// proof#120. An `anchor_ref` that failed structural validation is not
|
|
904
|
+
// an absent one. Nothing was fetched, so the witness dimension is NOT
|
|
905
|
+
// CHECKED — `onChain: null`, exactly as when the caller supplied no
|
|
906
|
+
// adapter — and it must not raise `gatewayUnavailable`, which is what
|
|
907
|
+
// puts "could not be re-fetched from the supplied gateways" on the
|
|
908
|
+
// bundle. Reporting an outage nobody observed is a claim about the
|
|
909
|
+
// public record made out of a defect in the artifact, and verify-page
|
|
910
|
+
// renders it word for word. The run still fails: the structural error
|
|
911
|
+
// is already in `cpr.errors` and already in `ok` above.
|
|
912
|
+
if (cpr.anchorRef === null && malformedAnchorRef.has(cpr)) {
|
|
913
|
+
cpr.onChain = null;
|
|
914
|
+
cpr.onChainOk = null;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
676
917
|
if (cpr.anchorRef === null) {
|
|
677
918
|
gatewayUnavailable = true;
|
|
678
919
|
cpr.onChain = {
|
|
@@ -686,9 +927,13 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
686
927
|
}
|
|
687
928
|
const outcome = await refetchCheckpoint(cp, cpr.anchorRef, gateways, fetchImpl);
|
|
688
929
|
cpr.onChain = outcome.onChain;
|
|
689
|
-
cpr.onChainOk = outcome.onChain
|
|
930
|
+
cpr.onChainOk = outcome.onChain?.onChainOk ?? null;
|
|
690
931
|
if (outcome.unavailable)
|
|
691
932
|
gatewayUnavailable = true;
|
|
933
|
+
if (outcome.unchecked === "kind")
|
|
934
|
+
uncheckedKind = true;
|
|
935
|
+
if (outcome.unchecked === "id")
|
|
936
|
+
uncheckedId = true;
|
|
692
937
|
if (outcome.error)
|
|
693
938
|
cpr.errors.push(outcome.error);
|
|
694
939
|
// Fold the on-chain dimension in: a MISMATCH (false) fails the
|
|
@@ -719,12 +964,16 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
719
964
|
if (anyHardFailure) {
|
|
720
965
|
status = "failed";
|
|
721
966
|
}
|
|
722
|
-
else if (gatewayUnavailable) {
|
|
967
|
+
else if (gatewayUnavailable || uncheckedKind || uncheckedId) {
|
|
723
968
|
// Every offline check passed and nothing MISMATCHED, but a requested
|
|
724
969
|
// on-chain confirmation could not be made — the bundle is offline-sound,
|
|
725
970
|
// its anchored state unconfirmed. "partial" + a note the CLI maps to exit 3.
|
|
726
971
|
status = "partial";
|
|
727
|
-
errors
|
|
972
|
+
pushRefetchGapNotes(errors, "checkpoints", {
|
|
973
|
+
unavailable: gatewayUnavailable,
|
|
974
|
+
kind: uncheckedKind,
|
|
975
|
+
id: uncheckedId,
|
|
976
|
+
});
|
|
728
977
|
}
|
|
729
978
|
else if (anyWithheldRecord) {
|
|
730
979
|
// Everything that COULD be checked passed; some record bytes were withheld
|
|
@@ -815,7 +1064,7 @@ function isNonNegInt(n) {
|
|
|
815
1064
|
// ===========================================================================
|
|
816
1065
|
// Disclosed-content binding, shared by every body that carries events.
|
|
817
1066
|
//
|
|
818
|
-
// Extracted for `sanning.stamp.evidence/
|
|
1067
|
+
// Extracted for `sanning.stamp.evidence/v2` (§5.3) rather than copied. The
|
|
819
1068
|
// rawLog -> content_hash link is the one step the chain
|
|
820
1069
|
// (content_hash -> record -> payload_hash -> signature -> leaf -> root) does
|
|
821
1070
|
// not otherwise cover, and a second copy of it is a second place for the
|
|
@@ -824,14 +1073,14 @@ function isNonNegInt(n) {
|
|
|
824
1073
|
// disclosed or nothing is committed; false ONLY on a real mismatch or an
|
|
825
1074
|
// in-body vs side-input disagreement.
|
|
826
1075
|
// ===========================================================================
|
|
827
|
-
async function verifyDisclosedContent(ev, eventId,
|
|
1076
|
+
async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
|
|
828
1077
|
const evErrors = [];
|
|
829
1078
|
let contentOk = null;
|
|
830
1079
|
const committed = [];
|
|
831
1080
|
let recordContentHash;
|
|
832
|
-
if (
|
|
1081
|
+
if (eventBytes !== undefined) {
|
|
833
1082
|
try {
|
|
834
|
-
const record =
|
|
1083
|
+
const record = jcsParse(eventBytes);
|
|
835
1084
|
const ch = record.event?.content_hash;
|
|
836
1085
|
if (typeof ch === "string" && ch)
|
|
837
1086
|
recordContentHash = ch.toLowerCase();
|
|
@@ -912,7 +1161,7 @@ async function verifyStampEntry(raw, seen) {
|
|
|
912
1161
|
bytes = hexToBytes(e.stamp_bytes);
|
|
913
1162
|
}
|
|
914
1163
|
catch (err) {
|
|
915
|
-
errors.push(`stamp_bytes is not hex
|
|
1164
|
+
errors.push(`stamp_bytes is not hex${thrownDetail(err)}`);
|
|
916
1165
|
return { result: base };
|
|
917
1166
|
}
|
|
918
1167
|
// Gated by `isLowerHex` above, so this is already the exact lowercase hex of
|
|
@@ -944,7 +1193,7 @@ async function verifyStampEntry(raw, seen) {
|
|
|
944
1193
|
// --- the body itself ------------------------------------------------------
|
|
945
1194
|
let body;
|
|
946
1195
|
try {
|
|
947
|
-
const parsed =
|
|
1196
|
+
const parsed = jcsParse(bytes);
|
|
948
1197
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
949
1198
|
errors.push("stamp body is not a JSON object");
|
|
950
1199
|
return { result: base, witnessedHex };
|
|
@@ -954,13 +1203,13 @@ async function verifyStampEntry(raw, seen) {
|
|
|
954
1203
|
// every submit attempt for exactly this reason: a non-canonical body makes
|
|
955
1204
|
// a retry a DIFFERENT submission, and makes every independent re-derivation
|
|
956
1205
|
// of the stamp disagree with the one that was witnessed.
|
|
957
|
-
if (jcs(body) !==
|
|
1206
|
+
if (jcs(body) !== decodeUtf8Strict(bytes)) {
|
|
958
1207
|
errors.push("stamp_bytes are not JCS-canonical (RFC 8785)");
|
|
959
1208
|
return { result: base, witnessedHex };
|
|
960
1209
|
}
|
|
961
1210
|
}
|
|
962
1211
|
catch (err) {
|
|
963
|
-
errors.push(`stamp body is not JSON
|
|
1212
|
+
errors.push(`stamp body is not JSON${thrownDetail(err)}`);
|
|
964
1213
|
return { result: base, witnessedHex };
|
|
965
1214
|
}
|
|
966
1215
|
if (body.stamp_version !== STAMP_BODY_VERSION) {
|
|
@@ -998,7 +1247,10 @@ async function verifyStampEntry(raw, seen) {
|
|
|
998
1247
|
}
|
|
999
1248
|
// --- the sub-tree ranges --------------------------------------------------
|
|
1000
1249
|
base.subtreesOk = checkSubtrees(body, base, errors);
|
|
1001
|
-
|
|
1250
|
+
// A stamp needs no malformed/absent split: the §5.2 re-fetch path already
|
|
1251
|
+
// leaves `onChain` null whenever there is no `AnchorRef`, so it never
|
|
1252
|
+
// synthesised the outage proof#120 is about.
|
|
1253
|
+
base.anchorRef = parseAnchorRef(e.anchor_ref, errors).ref;
|
|
1002
1254
|
base.ok =
|
|
1003
1255
|
base.stampRefOk && base.bodyOk && base.subtreesOk && errors.length === 0;
|
|
1004
1256
|
return { result: base, witnessedHex };
|
|
@@ -1080,7 +1332,7 @@ function checkSubtrees(body, out, errors) {
|
|
|
1080
1332
|
async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, options) {
|
|
1081
1333
|
const body = b.body;
|
|
1082
1334
|
if (!Array.isArray(body?.stamps)) {
|
|
1083
|
-
return malformed(`${STAMP_TRACE_BODY_TYPE} body must have stamps[]`, true);
|
|
1335
|
+
return malformed(`${STAMP_TRACE_BODY_TYPE} body must have stamps[]`, true, true);
|
|
1084
1336
|
}
|
|
1085
1337
|
const seen = new Set();
|
|
1086
1338
|
const stamps = [];
|
|
@@ -1097,7 +1349,7 @@ async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, option
|
|
|
1097
1349
|
// other — asks every consumer to remember which wrapper it is holding before
|
|
1098
1350
|
// it can trust `onChain: null`, and that is exactly the kind of per-body
|
|
1099
1351
|
// exception a reader eventually gets wrong.
|
|
1100
|
-
const { onChainChecked, gatewayUnavailable } = await refetchStamps(stamps, witnessed, options, errors);
|
|
1352
|
+
const { onChainChecked, gatewayUnavailable, uncheckedKind, uncheckedId } = await refetchStamps(stamps, witnessed, options, errors);
|
|
1101
1353
|
// --- Rollup ----------------------------------------------------------------
|
|
1102
1354
|
const anyHardFailure = stamps.some((s) => !s.ok);
|
|
1103
1355
|
const anyUnwitnessed = stamps.some((s) => s.anchorRef === null);
|
|
@@ -1112,13 +1364,15 @@ async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, option
|
|
|
1112
1364
|
status = "partial";
|
|
1113
1365
|
errors.push(`${STAMP_TRACE_BODY_TYPE} body carries no stamps`);
|
|
1114
1366
|
}
|
|
1115
|
-
else if (gatewayUnavailable || anyUnwitnessed) {
|
|
1367
|
+
else if (gatewayUnavailable || uncheckedKind || uncheckedId || anyUnwitnessed) {
|
|
1116
1368
|
// Both hold the bundle at `partial`, and both get said — see §5.3's rollup
|
|
1117
1369
|
// for why one note is not enough when there are two different gaps.
|
|
1118
1370
|
status = "partial";
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1371
|
+
pushRefetchGapNotes(errors, "stamps", {
|
|
1372
|
+
unavailable: gatewayUnavailable,
|
|
1373
|
+
kind: uncheckedKind,
|
|
1374
|
+
id: uncheckedId,
|
|
1375
|
+
});
|
|
1122
1376
|
if (anyUnwitnessed) {
|
|
1123
1377
|
// The honest half of P-10, one layer up. A stamp with no `anchor_ref` is
|
|
1124
1378
|
// sealed and unwitnessed: `retained-by-sanning`, which is a statement
|
|
@@ -1148,7 +1402,7 @@ async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, option
|
|
|
1148
1402
|
};
|
|
1149
1403
|
}
|
|
1150
1404
|
// ===========================================================================
|
|
1151
|
-
// `sanning.stamp.evidence/
|
|
1405
|
+
// `sanning.stamp.evidence/v2`: the two halves, bound (evidence-bundle.md §5.3)
|
|
1152
1406
|
//
|
|
1153
1407
|
// The stamp half is §5.2's code unchanged (`verifyStampEntry`, which carries
|
|
1154
1408
|
// the sub-tree partition check). The event half reuses the same primitives the
|
|
@@ -1165,7 +1419,7 @@ async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, option
|
|
|
1165
1419
|
async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, options) {
|
|
1166
1420
|
const body = b.body;
|
|
1167
1421
|
if (!Array.isArray(body?.stamps) || !Array.isArray(body?.events)) {
|
|
1168
|
-
return malformed(`${STAMP_EVIDENCE_BODY_TYPE} body must have stamps[] and events[]`, true);
|
|
1422
|
+
return malformed(`${STAMP_EVIDENCE_BODY_TYPE} body must have stamps[] and events[]`, true, true);
|
|
1169
1423
|
}
|
|
1170
1424
|
// --- The stamp half: §5.2's rules, §5.2's code ----------------------------
|
|
1171
1425
|
const seen = new Set();
|
|
@@ -1217,17 +1471,41 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1217
1471
|
const eventId = getEventId(ev.envelope);
|
|
1218
1472
|
let envelopeOk = false;
|
|
1219
1473
|
let payloadBindingOk = null;
|
|
1220
|
-
|
|
1474
|
+
// A retired field name under this major is a reject, never an ignored
|
|
1475
|
+
// unknown field: ignored, it reads as a withheld record (W-3).
|
|
1476
|
+
const legacyError = legacyEventBytesError(ev, "event", STAMP_EVIDENCE_BODY_TYPE);
|
|
1477
|
+
if (legacyError !== null)
|
|
1478
|
+
evErrors.push(legacyError);
|
|
1479
|
+
// MALFORMED IS A FAILURE; ABSENT IS UNDETERMINED (proof#85).
|
|
1480
|
+
//
|
|
1481
|
+
// `event_bytes` that will not decode leaves `eventBytes` undefined, and an
|
|
1482
|
+
// undefined `eventBytes` is exactly how a WITHHELD record is spelled one
|
|
1483
|
+
// line down. So an event carrying garbage in place of its record used to
|
|
1484
|
+
// reach the same tri-state as an event carrying nothing: `payloadHashOk`
|
|
1485
|
+
// null, `ok` true, a leading tick on the line, and a `verified` rollup over
|
|
1486
|
+
// a record nobody could read. The only thing wrong with the pack was the
|
|
1487
|
+
// bytes, and it was the one thing the verdict did not say.
|
|
1488
|
+
//
|
|
1489
|
+
// A record the producer chose not to disclose is the customer exercising
|
|
1490
|
+
// minimum disclosure. A record that is present and unreadable is a defect
|
|
1491
|
+
// in the artifact, and this kernel's standing rule (the same one
|
|
1492
|
+
// `checkpoints[]` already applies through `keyed.errors`) is that a defect
|
|
1493
|
+
// we can see fails.
|
|
1494
|
+
let eventBytes;
|
|
1495
|
+
let eventBytesMalformed = false;
|
|
1221
1496
|
try {
|
|
1222
|
-
if (typeof ev.
|
|
1223
|
-
|
|
1497
|
+
if (typeof ev.event_bytes === "string")
|
|
1498
|
+
eventBytes = hexToBytes(ev.event_bytes);
|
|
1224
1499
|
}
|
|
1225
1500
|
catch (e) {
|
|
1226
|
-
|
|
1501
|
+
eventBytesMalformed = true;
|
|
1502
|
+
evErrors.push(`event event_bytes is not hex${thrownDetail(e)}`);
|
|
1227
1503
|
}
|
|
1228
1504
|
try {
|
|
1229
|
-
const res = await verifyEnvelope(ev.envelope,
|
|
1230
|
-
|
|
1505
|
+
const res = await verifyEnvelope(ev.envelope, eventBytes !== undefined ? { payloadBytes: eventBytes } : {});
|
|
1506
|
+
const hexError = envelopeHexError("event", ev.envelope);
|
|
1507
|
+
// tolerates an undetermined binding (withheld record)
|
|
1508
|
+
envelopeOk = res.ok && hexError === null;
|
|
1231
1509
|
payloadBindingOk = res.payloadHashOk;
|
|
1232
1510
|
// P-10: report what could not be determined, never a fault that was never
|
|
1233
1511
|
// observed. There is no envelope here, so no signature was checked and no
|
|
@@ -1239,6 +1517,9 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1239
1517
|
if (!isJsonObject(ev.envelope)) {
|
|
1240
1518
|
evErrors.push("event envelope is not a JSON object");
|
|
1241
1519
|
}
|
|
1520
|
+
else if (hexError !== null) {
|
|
1521
|
+
evErrors.push(hexError);
|
|
1522
|
+
}
|
|
1242
1523
|
else if (!res.signatureOk) {
|
|
1243
1524
|
evErrors.push("event envelope signature failed");
|
|
1244
1525
|
}
|
|
@@ -1249,9 +1530,12 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1249
1530
|
for (const m of res.errors)
|
|
1250
1531
|
if (m.startsWith("malformed "))
|
|
1251
1532
|
evErrors.push(`event envelope ${m}`);
|
|
1533
|
+
const specFinding = specVersionFinding("event", ev.envelope, res);
|
|
1534
|
+
if (specFinding !== null)
|
|
1535
|
+
evErrors.push(specFinding);
|
|
1252
1536
|
}
|
|
1253
1537
|
catch (e) {
|
|
1254
|
-
evErrors.push(`event envelope verify error
|
|
1538
|
+
evErrors.push(`event envelope verify error${thrownDetail(e)}`);
|
|
1255
1539
|
}
|
|
1256
1540
|
// --- The join (§5.3.3 rule 1) -------------------------------------------
|
|
1257
1541
|
const incl = ev.inclusion;
|
|
@@ -1302,11 +1586,11 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1302
1586
|
}
|
|
1303
1587
|
}
|
|
1304
1588
|
catch (e) {
|
|
1305
|
-
evErrors.push(`inclusion verify error
|
|
1589
|
+
evErrors.push(`inclusion verify error${thrownDetail(e)}`);
|
|
1306
1590
|
}
|
|
1307
1591
|
}
|
|
1308
1592
|
}
|
|
1309
|
-
const contentRes = await verifyDisclosedContent(ev, eventId,
|
|
1593
|
+
const contentRes = await verifyDisclosedContent(ev, eventId, eventBytes, options.content);
|
|
1310
1594
|
const contentOk = contentRes.contentOk;
|
|
1311
1595
|
evErrors.push(...contentRes.errors);
|
|
1312
1596
|
eventResults.push({
|
|
@@ -1317,12 +1601,19 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1317
1601
|
// Named for §5.1's join; here it reports whether `stamp_ref` resolved.
|
|
1318
1602
|
checkpointBound: stampBound,
|
|
1319
1603
|
contentOk,
|
|
1320
|
-
|
|
1604
|
+
// `!eventBytesMalformed`: present-and-unreadable record bytes are a
|
|
1605
|
+
// defect in the artifact, never a disclosure choice (proof#85).
|
|
1606
|
+
ok: envelopeOk &&
|
|
1607
|
+
inclusionOk &&
|
|
1608
|
+
stampBound &&
|
|
1609
|
+
contentOk !== false &&
|
|
1610
|
+
legacyError === null &&
|
|
1611
|
+
!eventBytesMalformed,
|
|
1321
1612
|
errors: evErrors,
|
|
1322
1613
|
});
|
|
1323
1614
|
}
|
|
1324
1615
|
// --- The optional on-chain re-fetch (§5.2.4's "online act") ---------------
|
|
1325
|
-
const { onChainChecked, gatewayUnavailable } = await refetchStamps(stamps, witnessed, options, errors);
|
|
1616
|
+
const { onChainChecked, gatewayUnavailable, uncheckedKind, uncheckedId } = await refetchStamps(stamps, witnessed, options, errors);
|
|
1326
1617
|
// --- Rollup ---------------------------------------------------------------
|
|
1327
1618
|
// Rule 4: a declared event MUST be checked. `events[]` present and unverified
|
|
1328
1619
|
// is the false green this body exists to prevent, so an event that did not
|
|
@@ -1337,16 +1628,27 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1337
1628
|
status = "partial";
|
|
1338
1629
|
errors.push(`${STAMP_EVIDENCE_BODY_TYPE} body carries no stamps and no events`);
|
|
1339
1630
|
}
|
|
1340
|
-
else if (
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
//
|
|
1631
|
+
else if (eventResults.length === 0 ||
|
|
1632
|
+
gatewayUnavailable ||
|
|
1633
|
+
uncheckedKind ||
|
|
1634
|
+
uncheckedId ||
|
|
1635
|
+
anyUnwitnessed) {
|
|
1636
|
+
// Each holds the bundle at `partial`, and EACH gets said. A pack with no
|
|
1637
|
+
// events, a stamp nobody witnessed and a witness we could not reach are
|
|
1638
|
+
// different gaps with different fixes, and an auditor told only about one
|
|
1639
|
+
// would read the others as fine. The order is fixed so the two kernels
|
|
1640
|
+
// emit the same list.
|
|
1346
1641
|
status = "partial";
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1642
|
+
if (eventResults.length === 0) {
|
|
1643
|
+
// §5.3.2: an events-empty body is exactly a §5.2 stamp trace, and is
|
|
1644
|
+
// partial, never verified. It says nothing about any event.
|
|
1645
|
+
errors.push(`${STAMP_EVIDENCE_BODY_TYPE} body carries no events -- it evidences no event, only its stamps`);
|
|
1646
|
+
}
|
|
1647
|
+
pushRefetchGapNotes(errors, "stamps", {
|
|
1648
|
+
unavailable: gatewayUnavailable,
|
|
1649
|
+
kind: uncheckedKind,
|
|
1650
|
+
id: uncheckedId,
|
|
1651
|
+
});
|
|
1350
1652
|
if (anyUnwitnessed) {
|
|
1351
1653
|
// §5.3.3 rule 6, inherited whole from §5.2.4: for a stamp the witness IS
|
|
1352
1654
|
// the independent part, so the events resting on it inherit `partial`.
|
|
@@ -1407,14 +1709,17 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1407
1709
|
async function refetchStamps(stamps, witnessed, options, errors) {
|
|
1408
1710
|
const gateways = options.gateways ?? [];
|
|
1409
1711
|
const onChainChecked = gateways.length > 0;
|
|
1712
|
+
const none = { gatewayUnavailable: false, uncheckedKind: false, uncheckedId: false };
|
|
1410
1713
|
if (!onChainChecked)
|
|
1411
|
-
return { onChainChecked,
|
|
1714
|
+
return { onChainChecked, ...none };
|
|
1412
1715
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1413
1716
|
if (typeof fetchImpl !== "function") {
|
|
1414
1717
|
errors.push("on-chain re-fetch requested but no fetch implementation is available");
|
|
1415
|
-
return { onChainChecked, gatewayUnavailable: true };
|
|
1718
|
+
return { onChainChecked, ...none, gatewayUnavailable: true };
|
|
1416
1719
|
}
|
|
1417
1720
|
let gatewayUnavailable = false;
|
|
1721
|
+
let uncheckedKind = false;
|
|
1722
|
+
let uncheckedId = false;
|
|
1418
1723
|
for (let i = 0; i < stamps.length; i++) {
|
|
1419
1724
|
const st = stamps[i];
|
|
1420
1725
|
const witnessedHex = witnessed[i];
|
|
@@ -1424,9 +1729,13 @@ async function refetchStamps(stamps, witnessed, options, errors) {
|
|
|
1424
1729
|
continue;
|
|
1425
1730
|
const outcome = await refetchStamp(witnessedHex, st.anchorRef, gateways, fetchImpl);
|
|
1426
1731
|
st.onChain = outcome.onChain;
|
|
1427
|
-
st.onChainOk = outcome.onChain
|
|
1732
|
+
st.onChainOk = outcome.onChain?.onChainOk ?? null;
|
|
1428
1733
|
if (outcome.unavailable)
|
|
1429
1734
|
gatewayUnavailable = true;
|
|
1735
|
+
if (outcome.unchecked === "kind")
|
|
1736
|
+
uncheckedKind = true;
|
|
1737
|
+
if (outcome.unchecked === "id")
|
|
1738
|
+
uncheckedId = true;
|
|
1430
1739
|
if (outcome.error)
|
|
1431
1740
|
st.errors.push(outcome.error);
|
|
1432
1741
|
// Fold the on-chain dimension in exactly as a checkpoint does: a MISMATCH
|
|
@@ -1440,9 +1749,9 @@ async function refetchStamps(stamps, witnessed, options, errors) {
|
|
|
1440
1749
|
// unreachable note would fail a stamp for a gateway being down.
|
|
1441
1750
|
st.ok = st.ok && st.onChainOk !== false;
|
|
1442
1751
|
}
|
|
1443
|
-
return { onChainChecked, gatewayUnavailable };
|
|
1752
|
+
return { onChainChecked, gatewayUnavailable, uncheckedKind, uncheckedId };
|
|
1444
1753
|
}
|
|
1445
|
-
// How many `sanning.evidence.export/
|
|
1754
|
+
// How many `sanning.evidence.export/v2` wrappers deep the source bundle may be
|
|
1446
1755
|
// nested before this path gives up. verifyEvidenceBundle's canonicalization
|
|
1447
1756
|
// depth bound already rejects a pathological bundle, but the unwrap must be
|
|
1448
1757
|
// independently bounded rather than trusting that it ran first.
|
|
@@ -1475,7 +1784,7 @@ export async function disclosedContent(bundle, options = {}) {
|
|
|
1475
1784
|
});
|
|
1476
1785
|
const traceEvents = resolveTraceEvents(bundle);
|
|
1477
1786
|
if (traceEvents === undefined) {
|
|
1478
|
-
errors.push(`no ${
|
|
1787
|
+
errors.push(`no ${ANCHOR_TRACE_BODY_TYPE} / ${STAMP_EVIDENCE_BODY_TYPE} ` +
|
|
1479
1788
|
`body to materialize content from ` +
|
|
1480
1789
|
`(body_type ${JSON.stringify(verification.bodyType)})`);
|
|
1481
1790
|
return empty();
|
|
@@ -1562,7 +1871,7 @@ export async function disclosedContent(bundle, options = {}) {
|
|
|
1562
1871
|
// a caller must distinguish from a body that has events and no links.
|
|
1563
1872
|
//
|
|
1564
1873
|
// BOTH event-carrying bodies, not just anchor-trace (proof#73): §5.3's
|
|
1565
|
-
// `sanning.stamp.evidence/
|
|
1874
|
+
// `sanning.stamp.evidence/v2` is the hand-over pack, and it presents the same
|
|
1566
1875
|
// event shape.
|
|
1567
1876
|
//
|
|
1568
1877
|
// Exported for `chain.ts`, which needs the same descent and must not derive it
|
|
@@ -1578,15 +1887,14 @@ export function resolveTraceEvents(bundle) {
|
|
|
1578
1887
|
const b = cur;
|
|
1579
1888
|
const bodyType = typeof b.body_type === "string" ? b.body_type : null;
|
|
1580
1889
|
// Both event-carrying bodies, not just anchor-trace (proof#73). §5.3's
|
|
1581
|
-
// `sanning.stamp.evidence/
|
|
1582
|
-
// producer-signed envelope, optional `
|
|
1890
|
+
// `sanning.stamp.evidence/v2` presents the SAME event shape (a
|
|
1891
|
+
// producer-signed envelope, optional `event_bytes`, an inclusion proof),
|
|
1583
1892
|
// and it is the HAND-OVER PACK: the only artifact an auditor is ever
|
|
1584
1893
|
// handed. Dispatching on anchor-trace alone meant `readChains` reported
|
|
1585
1894
|
// zero chains on every pack, however well-formed the links inside its
|
|
1586
1895
|
// records were, so "is it in order?" read as NOT CHECKED on the one shape
|
|
1587
1896
|
// that matters. The writers have been emitting those links the whole time.
|
|
1588
1897
|
if (bodyType === ANCHOR_TRACE_BODY_TYPE ||
|
|
1589
|
-
bodyType === ANCHOR_TRACE_BODY_TYPE_V1 ||
|
|
1590
1898
|
bodyType === STAMP_EVIDENCE_BODY_TYPE) {
|
|
1591
1899
|
const body = b.body;
|
|
1592
1900
|
if (body === null || typeof body !== "object" || !Array.isArray(body.events))
|
|
@@ -1673,13 +1981,13 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
1673
1981
|
}
|
|
1674
1982
|
}
|
|
1675
1983
|
catch (e) {
|
|
1676
|
-
errors.push(`source_bundle canonicalization failed
|
|
1984
|
+
errors.push(`source_bundle canonicalization failed${thrownDetail(e)}`);
|
|
1677
1985
|
}
|
|
1678
1986
|
// --- Step 4: recompute the source verdict ---------------------------------
|
|
1679
1987
|
// The existing evidence path over the inline source bundle — same primitives
|
|
1680
1988
|
// (Ed25519 envelopes, payload binding, RFC 9162 inclusion, merkle roots,
|
|
1681
1989
|
// content_ok, and the optional per-gateway on-chain re-fetch, §4.2 / step 7).
|
|
1682
|
-
const sourceResult = await
|
|
1990
|
+
const sourceResult = await verifyExportSource(inline, options);
|
|
1683
1991
|
// Attestation data_hash binding (§5 6c).
|
|
1684
1992
|
const sourceCheckpoints = await indexSourceCheckpoints(inline);
|
|
1685
1993
|
// --- Step 6: embedded attestation records ---------------------------------
|
|
@@ -1717,7 +2025,7 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
1717
2025
|
}
|
|
1718
2026
|
}
|
|
1719
2027
|
catch (e) {
|
|
1720
|
-
errors.push(`verdict agreement comparison failed
|
|
2028
|
+
errors.push(`verdict agreement comparison failed${thrownDetail(e)}`);
|
|
1721
2029
|
}
|
|
1722
2030
|
// --- Step 7 already folded in by the source recompute ---------------------
|
|
1723
2031
|
// (verifyEvidenceBundle merged per-gateway outcomes into sourceResult.status:
|
|
@@ -1772,7 +2080,7 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
1772
2080
|
};
|
|
1773
2081
|
}
|
|
1774
2082
|
export async function recomputeExportVerdict(sourceBundle, attestationRecords, generatedAt, options = {}) {
|
|
1775
|
-
const sourceResult = await
|
|
2083
|
+
const sourceResult = await verifyExportSource(sourceBundle, options);
|
|
1776
2084
|
// Attestation data_hash binding (§5 6c) — the SAME index construction as
|
|
1777
2085
|
// verifyExportBody, deliberately shared rather than restated.
|
|
1778
2086
|
const sourceCheckpoints = await indexSourceCheckpoints(sourceBundle);
|
|
@@ -1812,6 +2120,54 @@ export async function recomputeExportVerdict(sourceBundle, attestationRecords, g
|
|
|
1812
2120
|
// field; otherwise a per-dimension AttestationResult whose `ok` gates on the
|
|
1813
2121
|
// three binding checks (sig / operator-address / data_hash). subject_ref is
|
|
1814
2122
|
// undetermined-tolerant and never gates.
|
|
2123
|
+
// The attestation payload fields evidence-export.md §3.1 marks Required, with
|
|
2124
|
+
// the type it gives each. A record whose payload lacks one, or carries another
|
|
2125
|
+
// type (null included), is malformed: §5 step 9 and §4.5 make "a missing
|
|
2126
|
+
// required field" an export the verifier cannot render (exit 2). §5 step 6
|
|
2127
|
+
// gates a WELL-FORMED record on signature, operator and data_hash only; these
|
|
2128
|
+
// checks decide whether the record is well-formed at all. `level` is the §3.1
|
|
2129
|
+
// enum 1 | 2 | 3. The integers are non-negative and within 2**53 - 1 (the
|
|
2130
|
+
// §3.1 clarification).
|
|
2131
|
+
const PAYLOAD_REQUIRED = [
|
|
2132
|
+
["attested_at", "string"],
|
|
2133
|
+
["tx_id", "string"],
|
|
2134
|
+
["data_hash", "string"],
|
|
2135
|
+
["data_size", "integer"],
|
|
2136
|
+
["block_height", "integer"],
|
|
2137
|
+
["block_timestamp", "integer"],
|
|
2138
|
+
["operator", "string"],
|
|
2139
|
+
["owner_address", "string"],
|
|
2140
|
+
["gateway", "string"],
|
|
2141
|
+
["signature_verified", "boolean"],
|
|
2142
|
+
["level", "level"],
|
|
2143
|
+
];
|
|
2144
|
+
function payloadFieldError(payload) {
|
|
2145
|
+
for (const [name, kind] of PAYLOAD_REQUIRED) {
|
|
2146
|
+
const v = Object.prototype.hasOwnProperty.call(payload, name) ? payload[name] : undefined;
|
|
2147
|
+
let ok;
|
|
2148
|
+
let want;
|
|
2149
|
+
if (kind === "string") {
|
|
2150
|
+
ok = typeof v === "string";
|
|
2151
|
+
want = "a string";
|
|
2152
|
+
}
|
|
2153
|
+
else if (kind === "boolean") {
|
|
2154
|
+
ok = typeof v === "boolean";
|
|
2155
|
+
want = "a boolean";
|
|
2156
|
+
}
|
|
2157
|
+
else if (kind === "integer") {
|
|
2158
|
+
// §3.1 (clarified): a non-negative integer no larger than 2**53 - 1.
|
|
2159
|
+
ok = typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
2160
|
+
want = "a non-negative integer";
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
ok = v === 1 || v === 2 || v === 3;
|
|
2164
|
+
want = "1, 2 or 3";
|
|
2165
|
+
}
|
|
2166
|
+
if (!ok)
|
|
2167
|
+
return `attestation payload field ${JSON.stringify(name)} is missing or not ${want}`;
|
|
2168
|
+
}
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
1815
2171
|
async function verifyAttestation(rec, sourceCheckpoints) {
|
|
1816
2172
|
const errs = [];
|
|
1817
2173
|
const result = {
|
|
@@ -1851,6 +2207,9 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
1851
2207
|
if (typeof rec.signature !== "string") {
|
|
1852
2208
|
return { result, malformed: "attestation record missing signature" };
|
|
1853
2209
|
}
|
|
2210
|
+
const fieldError = payloadFieldError(payload);
|
|
2211
|
+
if (fieldError !== null)
|
|
2212
|
+
return { result, malformed: fieldError };
|
|
1854
2213
|
const rsaKey = { kty: "RSA", n: pub.n, e: pub.e };
|
|
1855
2214
|
result.operator = typeof payload.operator === "string" ? payload.operator : "";
|
|
1856
2215
|
result.gateway = typeof payload.gateway === "string" ? payload.gateway : null;
|
|
@@ -1863,15 +2222,22 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
1863
2222
|
payloadBytes = utf8(jcs(payload));
|
|
1864
2223
|
}
|
|
1865
2224
|
catch (e) {
|
|
1866
|
-
return { result, malformed: `attestation payload canonicalization failed
|
|
2225
|
+
return { result, malformed: `attestation payload canonicalization failed${thrownDetail(e)}` };
|
|
1867
2226
|
}
|
|
1868
2227
|
try {
|
|
1869
2228
|
result.signatureOk = await verifyRsaPssSha256(payloadBytes, rec.signature, rsaKey);
|
|
1870
2229
|
if (!result.signatureOk)
|
|
1871
2230
|
errs.push("attestation RSA-PSS signature verification failed");
|
|
1872
2231
|
}
|
|
1873
|
-
catch
|
|
1874
|
-
|
|
2232
|
+
catch {
|
|
2233
|
+
// A FIXED sentence, not the library's. This kernel throws
|
|
2234
|
+
// `verifyRsaPssSha256: malformed signature hex: ...` and the Python kernel
|
|
2235
|
+
// raises `malformed signature hex: ...`, so quoting either one split the
|
|
2236
|
+
// two kernels on a finding a holder can put in an export body it signs,
|
|
2237
|
+
// and an export pack's findings reach a case reading through `reasons`
|
|
2238
|
+
// (gate 4 on proof#140, finding 1). Callers of `verifyRsaPssSha256` still
|
|
2239
|
+
// get this kernel's own words.
|
|
2240
|
+
return { result, malformed: "attestation signature or key is malformed" };
|
|
1875
2241
|
}
|
|
1876
2242
|
// (b) operator-address binding: base64url(SHA-256(modulus)) == payload.operator.
|
|
1877
2243
|
try {
|
|
@@ -1882,7 +2248,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
1882
2248
|
}
|
|
1883
2249
|
}
|
|
1884
2250
|
catch (e) {
|
|
1885
|
-
return { result, malformed: `attestation operator key
|
|
2251
|
+
return { result, malformed: `attestation operator key${thrownDetail(e)}` };
|
|
1886
2252
|
}
|
|
1887
2253
|
// (c) checkpoint resolution + data_hash binding. The checkpoint tx data IS the
|
|
1888
2254
|
// uploaded JCS(envelope) bytes (the same bytes the on-chain re-fetch compares
|
|
@@ -1904,7 +2270,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
1904
2270
|
}
|
|
1905
2271
|
}
|
|
1906
2272
|
catch (e) {
|
|
1907
|
-
errs.push(`cannot recompute checkpoint content hash
|
|
2273
|
+
errs.push(`cannot recompute checkpoint content hash${thrownDetail(e)}`);
|
|
1908
2274
|
}
|
|
1909
2275
|
}
|
|
1910
2276
|
// (d) subject_ref (§3.2) — well-formedness only (no side-input subject channel
|
|
@@ -2116,6 +2482,35 @@ async function refetchStamp(witnessedHex, anchorRef, gateways, fetchImpl) {
|
|
|
2116
2482
|
// beats unreachable" to drift, and the drifted copy is the one nobody reads.
|
|
2117
2483
|
// The subject only ever selects wording; it never changes a verdict.
|
|
2118
2484
|
async function refetchAnchored(subject, expectedHex, anchorRef, gateways, fetchImpl) {
|
|
2485
|
+
// Only a `kind` this kernel has a witness adapter for is re-fetched
|
|
2486
|
+
// (evidence-bundle.md §5.1: "where the verifier has an adapter for its
|
|
2487
|
+
// `kind`"; §5.1.1: an unrecognised kind MUST NOT fail verification). An `id`
|
|
2488
|
+
// of any other kind is not an Arweave id, so whatever a gateway serves at
|
|
2489
|
+
// that path says nothing about the witness, and comparing it would turn an
|
|
2490
|
+
// unrelated 200 into a false mismatch. No fetch, `onChain: null`, and the
|
|
2491
|
+
// run goes `partial`: the caller asked for the witness check and it was not
|
|
2492
|
+
// made.
|
|
2493
|
+
if (!WITNESS_ADAPTER_KINDS.has(anchorRef.kind)) {
|
|
2494
|
+
return {
|
|
2495
|
+
onChain: null,
|
|
2496
|
+
unavailable: false,
|
|
2497
|
+
unchecked: "kind",
|
|
2498
|
+
error: `no witness adapter for kind '${anchorRef.kind}'; the witness was not checked`,
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
// The id becomes a URL path segment. Anything but a 43-character base64url
|
|
2502
|
+
// transaction id can leave the gateway's base path once URL parsing
|
|
2503
|
+
// normalises it (`../x`, `%2e%2e/x`), or drop part of the path (`a#b`,
|
|
2504
|
+
// `a?b`), and the two kernels would then request different URLs. Such an id
|
|
2505
|
+
// is never fetched and never failed: the witness is not checked.
|
|
2506
|
+
if (!ARWEAVE_ID_RE.test(anchorRef.id)) {
|
|
2507
|
+
return {
|
|
2508
|
+
onChain: null,
|
|
2509
|
+
unavailable: false,
|
|
2510
|
+
unchecked: "id",
|
|
2511
|
+
error: "arweave witness id is not a 43-character base64url transaction id; the witness was not checked",
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2119
2514
|
const perGateway = [];
|
|
2120
2515
|
for (const gw of gateways) {
|
|
2121
2516
|
perGateway.push({
|
|
@@ -2134,7 +2529,28 @@ async function refetchAnchored(subject, expectedHex, anchorRef, gateways, fetchI
|
|
|
2134
2529
|
else if (onChain.rollup === "unreachable") {
|
|
2135
2530
|
error = `${subject} ${anchorRef.id} unreachable on all gateways`;
|
|
2136
2531
|
}
|
|
2137
|
-
return { onChain, unavailable: onChain.rollup === "unreachable", error };
|
|
2532
|
+
return { onChain, unavailable: onChain.rollup === "unreachable", unchecked: null, error };
|
|
2533
|
+
}
|
|
2534
|
+
// The `anchor_ref.kind` values this kernel can re-fetch from a gateway
|
|
2535
|
+
// (evidence-bundle.md §5.1.1 registers exactly `arweave`). Any other kind is
|
|
2536
|
+
// left unchecked by `refetchAnchored`, never probed.
|
|
2537
|
+
const WITNESS_ADAPTER_KINDS = new Set(["arweave"]);
|
|
2538
|
+
// The only `arweave` ids the adapter looks up: a 43-character base64url
|
|
2539
|
+
// transaction or data-item id.
|
|
2540
|
+
const ARWEAVE_ID_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
2541
|
+
// The bundle-level notes for a run where requested re-fetches were not made.
|
|
2542
|
+
// Each note keeps the phrase "could not be re-fetched", which is what both
|
|
2543
|
+
// CLIs map to exit 3. The order is fixed so the two kernels emit the same list.
|
|
2544
|
+
function pushRefetchGapNotes(errors, subject, gaps) {
|
|
2545
|
+
if (gaps.unavailable) {
|
|
2546
|
+
errors.push(`one or more ${subject} could not be re-fetched from the supplied gateways`);
|
|
2547
|
+
}
|
|
2548
|
+
if (gaps.kind) {
|
|
2549
|
+
errors.push(`one or more ${subject} name a witness kind this verifier has no adapter for, so it could not be re-fetched`);
|
|
2550
|
+
}
|
|
2551
|
+
if (gaps.id) {
|
|
2552
|
+
errors.push(`one or more ${subject} name an Arweave witness id this verifier cannot look up, so it could not be re-fetched`);
|
|
2553
|
+
}
|
|
2138
2554
|
}
|
|
2139
2555
|
// One gateway's outcome for one checkpoint tx: confirm (bytes match), mismatch
|
|
2140
2556
|
// (bytes disagree), or unreachable (no response / !ok / network error).
|
|
@@ -2165,7 +2581,7 @@ async function probeGateway(anchorRef, gateway, expectedHex, fetchImpl) {
|
|
|
2165
2581
|
if (toHex(onChainBytes) === expectedHex)
|
|
2166
2582
|
return "confirm";
|
|
2167
2583
|
try {
|
|
2168
|
-
const parsed =
|
|
2584
|
+
const parsed = jcsParse(onChainBytes);
|
|
2169
2585
|
if (toHex(utf8(jcs(parsed))) === expectedHex)
|
|
2170
2586
|
return "confirm";
|
|
2171
2587
|
}
|
|
@@ -2209,7 +2625,7 @@ function selectDisclosedBytes(ev, eventId, sideContent) {
|
|
|
2209
2625
|
inBody = hexToBytes(ev.content);
|
|
2210
2626
|
}
|
|
2211
2627
|
catch (e) {
|
|
2212
|
-
errors.push(`event content is not hex
|
|
2628
|
+
errors.push(`event content is not hex${thrownDetail(e)}`);
|
|
2213
2629
|
}
|
|
2214
2630
|
}
|
|
2215
2631
|
let side;
|
|
@@ -2222,7 +2638,7 @@ function selectDisclosedBytes(ev, eventId, sideContent) {
|
|
|
2222
2638
|
side = hexToBytes(sideEntry.value);
|
|
2223
2639
|
}
|
|
2224
2640
|
catch (e) {
|
|
2225
|
-
errors.push(`supplied content for ${eventId} is not hex
|
|
2641
|
+
errors.push(`supplied content for ${eventId} is not hex${thrownDetail(e)}`);
|
|
2226
2642
|
}
|
|
2227
2643
|
}
|
|
2228
2644
|
// `offered` comes from the SAME test that produced the value, never from
|