@sanning/proof 0.10.0 → 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 +4 -4
- package/dist/agent-proof.js +8 -2
- package/dist/case.d.ts +745 -0
- package/dist/case.js +1318 -0
- package/dist/chain.js +20 -1
- package/dist/cli.js +181 -27
- package/dist/crypto.d.ts +1 -0
- package/dist/crypto.js +29 -0
- package/dist/declaration.d.ts +284 -0
- package/dist/declaration.js +674 -0
- package/dist/evidence.js +296 -73
- package/dist/grading.d.ts +65 -4
- package/dist/grading.js +112 -6
- package/dist/handoff.d.ts +209 -0
- package/dist/handoff.js +598 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +50 -0
- 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 +150 -3
- package/package.json +1 -1
- package/src/agent-proof.ts +8 -2
- package/src/case.ts +1573 -0
- package/src/chain.ts +20 -1
- package/src/cli.ts +213 -29
- package/src/crypto.ts +28 -0
- package/src/declaration.ts +1005 -0
- package/src/evidence.ts +306 -71
- package/src/grading.ts +140 -6
- package/src/handoff.ts +674 -0
- package/src/index.ts +127 -0
- package/src/message-parts.ts +998 -0
- package/src/verifier.ts +150 -3
- package/vectors/README.md +36 -1
- package/vectors/jcs-reader.json +166 -0
- package/vectors/pack-grading.json +131 -3
package/dist/evidence.js
CHANGED
|
@@ -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
|
|
@@ -113,12 +118,23 @@ function isEvidenceMajorAccepted(specVersion) {
|
|
|
113
118
|
return minor.length > 0 && /^[0-9]+$/.test(minor);
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
|
-
|
|
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) {
|
|
117
133
|
return {
|
|
118
134
|
status: "malformed",
|
|
119
135
|
specVersionOk,
|
|
120
|
-
bodyHashOk:
|
|
121
|
-
signatureOk:
|
|
136
|
+
bodyHashOk: wrapperOk,
|
|
137
|
+
signatureOk: wrapperOk,
|
|
122
138
|
bodyType: null,
|
|
123
139
|
onChainChecked: false,
|
|
124
140
|
checkpoints: [],
|
|
@@ -127,8 +143,77 @@ function malformed(error, specVersionOk = false) {
|
|
|
127
143
|
errors: [error],
|
|
128
144
|
};
|
|
129
145
|
}
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
217
|
}
|
|
133
218
|
/**
|
|
134
219
|
* Spell a value inside a finding the way the Python kernel spells it.
|
|
@@ -189,26 +274,47 @@ async function verifyBundle(bundle, options, exportSource) {
|
|
|
189
274
|
}
|
|
190
275
|
// --- Step 2: verify the wrapper signature + recompute body_hash ------------
|
|
191
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.
|
|
192
285
|
let bodyHashOk = false;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
bodyHashOk = recomputed === b.body_hash;
|
|
196
|
-
if (!bodyHashOk) {
|
|
197
|
-
errors.push(`body_hash mismatch: bundle=${b.body_hash} recomputed=${recomputed}`);
|
|
198
|
-
}
|
|
286
|
+
if (!isHexField(b.body_hash, 64, true)) {
|
|
287
|
+
errors.push("wrapper body_hash is not 64 lowercase hex characters");
|
|
199
288
|
}
|
|
200
|
-
|
|
201
|
-
|
|
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
|
+
}
|
|
202
300
|
}
|
|
203
301
|
let signatureOk = false;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
318
|
}
|
|
213
319
|
const assertedStatus = typeof b.verdict?.status === "string" ? b.verdict.status : null;
|
|
214
320
|
// A broken wrapper (bad signature or tampered body) is a hard failure — the
|
|
@@ -345,31 +451,39 @@ async function indexSourceCheckpoints(bundle) {
|
|
|
345
451
|
*/
|
|
346
452
|
async function resolveCheckpointKey(cp, eventBytes) {
|
|
347
453
|
const errors = [];
|
|
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 };
|
|
348
457
|
if (cp.tx_id !== undefined) {
|
|
349
458
|
errors.push(`${ANCHOR_TRACE_BODY_TYPE} checkpoint carries legacy tx_id -- one bundle speaks one vocabulary`);
|
|
350
|
-
return
|
|
459
|
+
return unexamined;
|
|
351
460
|
}
|
|
352
461
|
const legacy = legacyEventBytesError(cp, "checkpoint", ANCHOR_TRACE_BODY_TYPE);
|
|
353
462
|
if (legacy !== null) {
|
|
354
463
|
errors.push(legacy);
|
|
355
|
-
return
|
|
464
|
+
return unexamined;
|
|
356
465
|
}
|
|
357
466
|
if (typeof cp.checkpoint_ref !== "string" || cp.checkpoint_ref === "") {
|
|
358
467
|
errors.push("checkpoint missing checkpoint_ref");
|
|
359
|
-
return
|
|
468
|
+
return unexamined;
|
|
360
469
|
}
|
|
361
470
|
if (eventBytes === undefined) {
|
|
362
471
|
// Without the bytes the key cannot be recomputed, so it cannot be trusted.
|
|
363
472
|
errors.push("checkpoint_ref cannot be verified -- event_bytes missing or not hex");
|
|
364
|
-
return
|
|
473
|
+
return unexamined;
|
|
365
474
|
}
|
|
366
475
|
const derived = await sha256Hex(eventBytes);
|
|
367
476
|
if (derived !== cp.checkpoint_ref.toLowerCase()) {
|
|
368
477
|
errors.push(`checkpoint_ref does not match SHA-256(event_bytes): claimed=${cp.checkpoint_ref} derived=${derived}`);
|
|
369
|
-
return
|
|
478
|
+
return unexamined;
|
|
370
479
|
}
|
|
371
|
-
const
|
|
372
|
-
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
|
+
};
|
|
373
487
|
}
|
|
374
488
|
/**
|
|
375
489
|
* Validate an optional `anchor_ref` (§5.1.1).
|
|
@@ -388,39 +502,54 @@ async function resolveCheckpointKey(cp, eventBytes) {
|
|
|
388
502
|
* So this validates the SHAPE and never the vocabulary. A malformed
|
|
389
503
|
* `anchor_ref` is a defect in the bundle; an unknown `kind` is a defect in
|
|
390
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.
|
|
391
516
|
*/
|
|
392
517
|
function parseAnchorRef(raw, errors) {
|
|
393
|
-
if (raw === undefined || raw === null)
|
|
394
|
-
return null; // placed, not yet witnessed
|
|
518
|
+
if (raw === undefined || raw === null) {
|
|
519
|
+
return { ref: null, malformed: false }; // placed, not yet witnessed
|
|
520
|
+
}
|
|
395
521
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
396
522
|
errors.push("anchor_ref must be an object");
|
|
397
|
-
return null;
|
|
523
|
+
return { ref: null, malformed: true };
|
|
398
524
|
}
|
|
399
525
|
const o = raw;
|
|
400
526
|
if (typeof o.kind !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(o.kind)) {
|
|
401
527
|
errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind)}`);
|
|
402
|
-
return null;
|
|
528
|
+
return { ref: null, malformed: true };
|
|
403
529
|
}
|
|
404
530
|
if (typeof o.id !== "string" || o.id === "") {
|
|
405
531
|
errors.push("anchor_ref.id is missing or empty");
|
|
406
|
-
return null;
|
|
532
|
+
return { ref: null, malformed: true };
|
|
407
533
|
}
|
|
408
534
|
const ref = o.ref;
|
|
409
535
|
if (ref !== undefined && (typeof ref !== "object" || ref === null || Array.isArray(ref))) {
|
|
410
536
|
errors.push("anchor_ref.ref must be an object when present");
|
|
411
|
-
return null;
|
|
537
|
+
return { ref: null, malformed: true };
|
|
412
538
|
}
|
|
413
539
|
return {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
540
|
+
ref: {
|
|
541
|
+
kind: o.kind,
|
|
542
|
+
id: o.id,
|
|
543
|
+
...(ref === undefined ? {} : { ref: ref }),
|
|
544
|
+
},
|
|
545
|
+
malformed: false,
|
|
417
546
|
};
|
|
418
547
|
}
|
|
419
548
|
async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, options) {
|
|
420
549
|
const body = b.body;
|
|
421
550
|
const bodyType = ANCHOR_TRACE_BODY_TYPE;
|
|
422
551
|
if (!Array.isArray(body.checkpoints) || !Array.isArray(body.events)) {
|
|
423
|
-
return malformed(`${bodyType} body must have checkpoints[] and events[]`, true);
|
|
552
|
+
return malformed(`${bodyType} body must have checkpoints[] and events[]`, true, true);
|
|
424
553
|
}
|
|
425
554
|
const gateways = options.gateways ?? [];
|
|
426
555
|
const onChainChecked = gateways.length > 0;
|
|
@@ -435,6 +564,11 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
435
564
|
const byKey = new Map();
|
|
436
565
|
const anchorByKey = new Map();
|
|
437
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();
|
|
438
572
|
for (const rawCp of body.checkpoints) {
|
|
439
573
|
// `checkpoints[]` is attacker-shaped, and a verifier must never raise on
|
|
440
574
|
// adversarial input: a null entry used to reach `cp.tx_id` and hand the
|
|
@@ -469,7 +603,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
469
603
|
eventBytes = hexToBytes(cp.event_bytes);
|
|
470
604
|
}
|
|
471
605
|
catch (e) {
|
|
472
|
-
cpErrors.push(`checkpoint event_bytes is not hex
|
|
606
|
+
cpErrors.push(`checkpoint event_bytes is not hex${thrownDetail(e)}`);
|
|
473
607
|
}
|
|
474
608
|
const keyed = await resolveCheckpointKey(cp, eventBytes);
|
|
475
609
|
cpErrors.push(...keyed.errors);
|
|
@@ -500,12 +634,16 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
500
634
|
// A checkpoint must fully bind: signature + the committed record matching
|
|
501
635
|
// payload_hash. An undetermined binding (no record) is not enough to
|
|
502
636
|
// anchor the events' inclusion claims to a trustworthy root.
|
|
503
|
-
|
|
637
|
+
const hexError = envelopeHexError("checkpoint", cp.envelope);
|
|
638
|
+
envelopeOk = res.ok && res.payloadHashOk === true && hexError === null;
|
|
504
639
|
// See the event loop below: a degenerate envelope is a malformed input,
|
|
505
640
|
// not a failed check (proof#76).
|
|
506
641
|
if (!isJsonObject(cp.envelope)) {
|
|
507
642
|
cpErrors.push("checkpoint envelope is not a JSON object");
|
|
508
643
|
}
|
|
644
|
+
else if (hexError !== null) {
|
|
645
|
+
cpErrors.push(hexError);
|
|
646
|
+
}
|
|
509
647
|
else if (!res.signatureOk) {
|
|
510
648
|
cpErrors.push("checkpoint envelope signature failed");
|
|
511
649
|
}
|
|
@@ -531,13 +669,13 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
531
669
|
}
|
|
532
670
|
}
|
|
533
671
|
catch (e) {
|
|
534
|
-
cpErrors.push(`checkpoint envelope verify error
|
|
672
|
+
cpErrors.push(`checkpoint envelope verify error${thrownDetail(e)}`);
|
|
535
673
|
}
|
|
536
674
|
// The committed record's merkle_root must equal the checkpoint's claimed
|
|
537
675
|
// merkle_root (recompute, don't trust the bundle's top-level claim).
|
|
538
676
|
if (eventBytes !== undefined) {
|
|
539
677
|
try {
|
|
540
|
-
const record =
|
|
678
|
+
const record = jcsParse(eventBytes);
|
|
541
679
|
const committedRoot = record.event?.merkle_root;
|
|
542
680
|
merkleRootOk = typeof committedRoot === "string" && committedRoot === cp.merkle_root;
|
|
543
681
|
if (!merkleRootOk) {
|
|
@@ -546,7 +684,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
546
684
|
}
|
|
547
685
|
}
|
|
548
686
|
catch (e) {
|
|
549
|
-
cpErrors.push(`checkpoint record is not JSON
|
|
687
|
+
cpErrors.push(`checkpoint record is not JSON${thrownDetail(e)}`);
|
|
550
688
|
}
|
|
551
689
|
}
|
|
552
690
|
checkpointResults.push({
|
|
@@ -565,6 +703,9 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
565
703
|
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0 && !duplicateKey,
|
|
566
704
|
errors: cpErrors,
|
|
567
705
|
});
|
|
706
|
+
if (keyed.anchorRefMalformed) {
|
|
707
|
+
malformedAnchorRef.add(checkpointResults[checkpointResults.length - 1]);
|
|
708
|
+
}
|
|
568
709
|
}
|
|
569
710
|
// --- Step 4: per-event ------------------------------------------------------
|
|
570
711
|
const eventResults = [];
|
|
@@ -595,17 +736,36 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
595
736
|
const legacyError = legacyEventBytesError(ev, "event", bodyType);
|
|
596
737
|
if (legacyError !== null)
|
|
597
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.
|
|
598
754
|
let eventBytes;
|
|
755
|
+
let eventBytesMalformed = false;
|
|
599
756
|
try {
|
|
600
757
|
if (typeof ev.event_bytes === "string")
|
|
601
758
|
eventBytes = hexToBytes(ev.event_bytes);
|
|
602
759
|
}
|
|
603
760
|
catch (e) {
|
|
604
|
-
|
|
761
|
+
eventBytesMalformed = true;
|
|
762
|
+
evErrors.push(`event event_bytes is not hex${thrownDetail(e)}`);
|
|
605
763
|
}
|
|
606
764
|
try {
|
|
607
765
|
const res = await verifyEnvelope(ev.envelope, eventBytes !== undefined ? { payloadBytes: eventBytes } : {});
|
|
608
|
-
|
|
766
|
+
const hexError = envelopeHexError("event", ev.envelope);
|
|
767
|
+
// ok tolerates an undetermined binding (withheld record)
|
|
768
|
+
envelopeOk = res.ok && hexError === null;
|
|
609
769
|
payloadBindingOk = res.payloadHashOk;
|
|
610
770
|
// P-10: report what could not be determined, never a fault that was never
|
|
611
771
|
// observed. There is no envelope here, so no signature was checked and no
|
|
@@ -617,6 +777,9 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
617
777
|
if (!isJsonObject(ev.envelope)) {
|
|
618
778
|
evErrors.push("event envelope is not a JSON object");
|
|
619
779
|
}
|
|
780
|
+
else if (hexError !== null) {
|
|
781
|
+
evErrors.push(hexError);
|
|
782
|
+
}
|
|
620
783
|
else if (!res.signatureOk) {
|
|
621
784
|
evErrors.push("event envelope signature failed");
|
|
622
785
|
}
|
|
@@ -632,7 +795,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
632
795
|
evErrors.push(specFinding);
|
|
633
796
|
}
|
|
634
797
|
catch (e) {
|
|
635
|
-
evErrors.push(`event envelope verify error
|
|
798
|
+
evErrors.push(`event envelope verify error${thrownDetail(e)}`);
|
|
636
799
|
}
|
|
637
800
|
// Bind the event to exactly one checkpoint in the bundle, then verify
|
|
638
801
|
// RFC 9162 inclusion against THAT checkpoint's merkle_root.
|
|
@@ -686,7 +849,7 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
686
849
|
}
|
|
687
850
|
}
|
|
688
851
|
catch (e) {
|
|
689
|
-
evErrors.push(`inclusion verify error
|
|
852
|
+
evErrors.push(`inclusion verify error${thrownDetail(e)}`);
|
|
690
853
|
}
|
|
691
854
|
}
|
|
692
855
|
// Content (raw-log) binding — shared with every other event-carrying body.
|
|
@@ -705,7 +868,14 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
705
868
|
// present checkpoint, and its disclosed content did not MISMATCH. A
|
|
706
869
|
// withheld record or undisclosed content (binding/content null) does not
|
|
707
870
|
// fail it.
|
|
708
|
-
|
|
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,
|
|
709
879
|
errors: evErrors,
|
|
710
880
|
});
|
|
711
881
|
}
|
|
@@ -730,6 +900,20 @@ async function verifyAnchorTrace(b, specVersionOk, assertedStatus, errors, optio
|
|
|
730
900
|
// the run to partial) and NEVER as a mismatch. Treating "we never
|
|
731
901
|
// anchored it yet" as "the chain disagrees" would be the same false
|
|
732
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
|
+
}
|
|
733
917
|
if (cpr.anchorRef === null) {
|
|
734
918
|
gatewayUnavailable = true;
|
|
735
919
|
cpr.onChain = {
|
|
@@ -896,7 +1080,7 @@ async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
|
|
|
896
1080
|
let recordContentHash;
|
|
897
1081
|
if (eventBytes !== undefined) {
|
|
898
1082
|
try {
|
|
899
|
-
const record =
|
|
1083
|
+
const record = jcsParse(eventBytes);
|
|
900
1084
|
const ch = record.event?.content_hash;
|
|
901
1085
|
if (typeof ch === "string" && ch)
|
|
902
1086
|
recordContentHash = ch.toLowerCase();
|
|
@@ -977,7 +1161,7 @@ async function verifyStampEntry(raw, seen) {
|
|
|
977
1161
|
bytes = hexToBytes(e.stamp_bytes);
|
|
978
1162
|
}
|
|
979
1163
|
catch (err) {
|
|
980
|
-
errors.push(`stamp_bytes is not hex
|
|
1164
|
+
errors.push(`stamp_bytes is not hex${thrownDetail(err)}`);
|
|
981
1165
|
return { result: base };
|
|
982
1166
|
}
|
|
983
1167
|
// Gated by `isLowerHex` above, so this is already the exact lowercase hex of
|
|
@@ -1009,7 +1193,7 @@ async function verifyStampEntry(raw, seen) {
|
|
|
1009
1193
|
// --- the body itself ------------------------------------------------------
|
|
1010
1194
|
let body;
|
|
1011
1195
|
try {
|
|
1012
|
-
const parsed =
|
|
1196
|
+
const parsed = jcsParse(bytes);
|
|
1013
1197
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1014
1198
|
errors.push("stamp body is not a JSON object");
|
|
1015
1199
|
return { result: base, witnessedHex };
|
|
@@ -1019,13 +1203,13 @@ async function verifyStampEntry(raw, seen) {
|
|
|
1019
1203
|
// every submit attempt for exactly this reason: a non-canonical body makes
|
|
1020
1204
|
// a retry a DIFFERENT submission, and makes every independent re-derivation
|
|
1021
1205
|
// of the stamp disagree with the one that was witnessed.
|
|
1022
|
-
if (jcs(body) !==
|
|
1206
|
+
if (jcs(body) !== decodeUtf8Strict(bytes)) {
|
|
1023
1207
|
errors.push("stamp_bytes are not JCS-canonical (RFC 8785)");
|
|
1024
1208
|
return { result: base, witnessedHex };
|
|
1025
1209
|
}
|
|
1026
1210
|
}
|
|
1027
1211
|
catch (err) {
|
|
1028
|
-
errors.push(`stamp body is not JSON
|
|
1212
|
+
errors.push(`stamp body is not JSON${thrownDetail(err)}`);
|
|
1029
1213
|
return { result: base, witnessedHex };
|
|
1030
1214
|
}
|
|
1031
1215
|
if (body.stamp_version !== STAMP_BODY_VERSION) {
|
|
@@ -1063,7 +1247,10 @@ async function verifyStampEntry(raw, seen) {
|
|
|
1063
1247
|
}
|
|
1064
1248
|
// --- the sub-tree ranges --------------------------------------------------
|
|
1065
1249
|
base.subtreesOk = checkSubtrees(body, base, errors);
|
|
1066
|
-
|
|
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;
|
|
1067
1254
|
base.ok =
|
|
1068
1255
|
base.stampRefOk && base.bodyOk && base.subtreesOk && errors.length === 0;
|
|
1069
1256
|
return { result: base, witnessedHex };
|
|
@@ -1145,7 +1332,7 @@ function checkSubtrees(body, out, errors) {
|
|
|
1145
1332
|
async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, options) {
|
|
1146
1333
|
const body = b.body;
|
|
1147
1334
|
if (!Array.isArray(body?.stamps)) {
|
|
1148
|
-
return malformed(`${STAMP_TRACE_BODY_TYPE} body must have stamps[]`, true);
|
|
1335
|
+
return malformed(`${STAMP_TRACE_BODY_TYPE} body must have stamps[]`, true, true);
|
|
1149
1336
|
}
|
|
1150
1337
|
const seen = new Set();
|
|
1151
1338
|
const stamps = [];
|
|
@@ -1232,7 +1419,7 @@ async function verifyStampTrace(b, specVersionOk, assertedStatus, errors, option
|
|
|
1232
1419
|
async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, options) {
|
|
1233
1420
|
const body = b.body;
|
|
1234
1421
|
if (!Array.isArray(body?.stamps) || !Array.isArray(body?.events)) {
|
|
1235
|
-
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);
|
|
1236
1423
|
}
|
|
1237
1424
|
// --- The stamp half: §5.2's rules, §5.2's code ----------------------------
|
|
1238
1425
|
const seen = new Set();
|
|
@@ -1289,17 +1476,36 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1289
1476
|
const legacyError = legacyEventBytesError(ev, "event", STAMP_EVIDENCE_BODY_TYPE);
|
|
1290
1477
|
if (legacyError !== null)
|
|
1291
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.
|
|
1292
1494
|
let eventBytes;
|
|
1495
|
+
let eventBytesMalformed = false;
|
|
1293
1496
|
try {
|
|
1294
1497
|
if (typeof ev.event_bytes === "string")
|
|
1295
1498
|
eventBytes = hexToBytes(ev.event_bytes);
|
|
1296
1499
|
}
|
|
1297
1500
|
catch (e) {
|
|
1298
|
-
|
|
1501
|
+
eventBytesMalformed = true;
|
|
1502
|
+
evErrors.push(`event event_bytes is not hex${thrownDetail(e)}`);
|
|
1299
1503
|
}
|
|
1300
1504
|
try {
|
|
1301
1505
|
const res = await verifyEnvelope(ev.envelope, eventBytes !== undefined ? { payloadBytes: eventBytes } : {});
|
|
1302
|
-
|
|
1506
|
+
const hexError = envelopeHexError("event", ev.envelope);
|
|
1507
|
+
// tolerates an undetermined binding (withheld record)
|
|
1508
|
+
envelopeOk = res.ok && hexError === null;
|
|
1303
1509
|
payloadBindingOk = res.payloadHashOk;
|
|
1304
1510
|
// P-10: report what could not be determined, never a fault that was never
|
|
1305
1511
|
// observed. There is no envelope here, so no signature was checked and no
|
|
@@ -1311,6 +1517,9 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1311
1517
|
if (!isJsonObject(ev.envelope)) {
|
|
1312
1518
|
evErrors.push("event envelope is not a JSON object");
|
|
1313
1519
|
}
|
|
1520
|
+
else if (hexError !== null) {
|
|
1521
|
+
evErrors.push(hexError);
|
|
1522
|
+
}
|
|
1314
1523
|
else if (!res.signatureOk) {
|
|
1315
1524
|
evErrors.push("event envelope signature failed");
|
|
1316
1525
|
}
|
|
@@ -1326,7 +1535,7 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1326
1535
|
evErrors.push(specFinding);
|
|
1327
1536
|
}
|
|
1328
1537
|
catch (e) {
|
|
1329
|
-
evErrors.push(`event envelope verify error
|
|
1538
|
+
evErrors.push(`event envelope verify error${thrownDetail(e)}`);
|
|
1330
1539
|
}
|
|
1331
1540
|
// --- The join (§5.3.3 rule 1) -------------------------------------------
|
|
1332
1541
|
const incl = ev.inclusion;
|
|
@@ -1377,7 +1586,7 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1377
1586
|
}
|
|
1378
1587
|
}
|
|
1379
1588
|
catch (e) {
|
|
1380
|
-
evErrors.push(`inclusion verify error
|
|
1589
|
+
evErrors.push(`inclusion verify error${thrownDetail(e)}`);
|
|
1381
1590
|
}
|
|
1382
1591
|
}
|
|
1383
1592
|
}
|
|
@@ -1392,7 +1601,14 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
|
|
|
1392
1601
|
// Named for §5.1's join; here it reports whether `stamp_ref` resolved.
|
|
1393
1602
|
checkpointBound: stampBound,
|
|
1394
1603
|
contentOk,
|
|
1395
|
-
|
|
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,
|
|
1396
1612
|
errors: evErrors,
|
|
1397
1613
|
});
|
|
1398
1614
|
}
|
|
@@ -1765,7 +1981,7 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
1765
1981
|
}
|
|
1766
1982
|
}
|
|
1767
1983
|
catch (e) {
|
|
1768
|
-
errors.push(`source_bundle canonicalization failed
|
|
1984
|
+
errors.push(`source_bundle canonicalization failed${thrownDetail(e)}`);
|
|
1769
1985
|
}
|
|
1770
1986
|
// --- Step 4: recompute the source verdict ---------------------------------
|
|
1771
1987
|
// The existing evidence path over the inline source bundle — same primitives
|
|
@@ -1809,7 +2025,7 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
|
|
|
1809
2025
|
}
|
|
1810
2026
|
}
|
|
1811
2027
|
catch (e) {
|
|
1812
|
-
errors.push(`verdict agreement comparison failed
|
|
2028
|
+
errors.push(`verdict agreement comparison failed${thrownDetail(e)}`);
|
|
1813
2029
|
}
|
|
1814
2030
|
// --- Step 7 already folded in by the source recompute ---------------------
|
|
1815
2031
|
// (verifyEvidenceBundle merged per-gateway outcomes into sourceResult.status:
|
|
@@ -2006,15 +2222,22 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
2006
2222
|
payloadBytes = utf8(jcs(payload));
|
|
2007
2223
|
}
|
|
2008
2224
|
catch (e) {
|
|
2009
|
-
return { result, malformed: `attestation payload canonicalization failed
|
|
2225
|
+
return { result, malformed: `attestation payload canonicalization failed${thrownDetail(e)}` };
|
|
2010
2226
|
}
|
|
2011
2227
|
try {
|
|
2012
2228
|
result.signatureOk = await verifyRsaPssSha256(payloadBytes, rec.signature, rsaKey);
|
|
2013
2229
|
if (!result.signatureOk)
|
|
2014
2230
|
errs.push("attestation RSA-PSS signature verification failed");
|
|
2015
2231
|
}
|
|
2016
|
-
catch
|
|
2017
|
-
|
|
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" };
|
|
2018
2241
|
}
|
|
2019
2242
|
// (b) operator-address binding: base64url(SHA-256(modulus)) == payload.operator.
|
|
2020
2243
|
try {
|
|
@@ -2025,7 +2248,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
2025
2248
|
}
|
|
2026
2249
|
}
|
|
2027
2250
|
catch (e) {
|
|
2028
|
-
return { result, malformed: `attestation operator key
|
|
2251
|
+
return { result, malformed: `attestation operator key${thrownDetail(e)}` };
|
|
2029
2252
|
}
|
|
2030
2253
|
// (c) checkpoint resolution + data_hash binding. The checkpoint tx data IS the
|
|
2031
2254
|
// uploaded JCS(envelope) bytes (the same bytes the on-chain re-fetch compares
|
|
@@ -2047,7 +2270,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
|
|
|
2047
2270
|
}
|
|
2048
2271
|
}
|
|
2049
2272
|
catch (e) {
|
|
2050
|
-
errs.push(`cannot recompute checkpoint content hash
|
|
2273
|
+
errs.push(`cannot recompute checkpoint content hash${thrownDetail(e)}`);
|
|
2051
2274
|
}
|
|
2052
2275
|
}
|
|
2053
2276
|
// (d) subject_ref (§3.2) — well-formedness only (no side-input subject channel
|
|
@@ -2358,7 +2581,7 @@ async function probeGateway(anchorRef, gateway, expectedHex, fetchImpl) {
|
|
|
2358
2581
|
if (toHex(onChainBytes) === expectedHex)
|
|
2359
2582
|
return "confirm";
|
|
2360
2583
|
try {
|
|
2361
|
-
const parsed =
|
|
2584
|
+
const parsed = jcsParse(onChainBytes);
|
|
2362
2585
|
if (toHex(utf8(jcs(parsed))) === expectedHex)
|
|
2363
2586
|
return "confirm";
|
|
2364
2587
|
}
|
|
@@ -2402,7 +2625,7 @@ function selectDisclosedBytes(ev, eventId, sideContent) {
|
|
|
2402
2625
|
inBody = hexToBytes(ev.content);
|
|
2403
2626
|
}
|
|
2404
2627
|
catch (e) {
|
|
2405
|
-
errors.push(`event content is not hex
|
|
2628
|
+
errors.push(`event content is not hex${thrownDetail(e)}`);
|
|
2406
2629
|
}
|
|
2407
2630
|
}
|
|
2408
2631
|
let side;
|
|
@@ -2415,7 +2638,7 @@ function selectDisclosedBytes(ev, eventId, sideContent) {
|
|
|
2415
2638
|
side = hexToBytes(sideEntry.value);
|
|
2416
2639
|
}
|
|
2417
2640
|
catch (e) {
|
|
2418
|
-
errors.push(`supplied content for ${eventId} is not hex
|
|
2641
|
+
errors.push(`supplied content for ${eventId} is not hex${thrownDetail(e)}`);
|
|
2419
2642
|
}
|
|
2420
2643
|
}
|
|
2421
2644
|
// `offered` comes from the SAME test that produced the value, never from
|