@sanning/proof 0.5.0 → 0.6.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 +31 -4
- package/dist/cli.js +212 -37
- package/dist/compose.js +1 -1
- package/dist/evidence.d.ts +77 -7
- package/dist/evidence.js +284 -66
- package/dist/grading.d.ts +160 -0
- package/dist/grading.js +469 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +17 -1
- package/dist/side-content.d.ts +36 -0
- package/dist/side-content.js +98 -0
- package/package.json +2 -1
- package/src/cli.ts +275 -35
- package/src/compose.ts +1 -1
- package/src/evidence.ts +394 -80
- package/src/grading.ts +623 -0
- package/src/index.ts +37 -0
- package/src/side-content.ts +119 -0
- package/vectors/README.md +66 -0
- package/vectors/pack-grading.json +136 -0
package/src/evidence.ts
CHANGED
|
@@ -32,6 +32,11 @@ import {
|
|
|
32
32
|
jcs,
|
|
33
33
|
verifyEnvelope,
|
|
34
34
|
} from "./verifier.js";
|
|
35
|
+
import {
|
|
36
|
+
inBodyContentOffered,
|
|
37
|
+
sideContentEntry,
|
|
38
|
+
type SideEntry,
|
|
39
|
+
} from "./side-content.js";
|
|
35
40
|
import type { Envelope } from "./types.js";
|
|
36
41
|
|
|
37
42
|
// Only the evidence-bundle major(s) this kernel verifies. A new major is a
|
|
@@ -43,7 +48,23 @@ const ACCEPTED_EVIDENCE_MAJORS = ["sanning.evidence/v1"];
|
|
|
43
48
|
// body_type is not a hard failure of the WRAPPER (the signature + body_hash
|
|
44
49
|
// still verify), but its body cannot be re-derived, so the per-body verdict is
|
|
45
50
|
// "undetermined" rather than "verified".
|
|
46
|
-
export const ANCHOR_TRACE_BODY_TYPE = "sanning.anchor.trace/
|
|
51
|
+
export const ANCHOR_TRACE_BODY_TYPE = "sanning.anchor.trace/v2";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The frozen pre-`anchor_ref` body (evidence-bundle.md §5.1.3).
|
|
55
|
+
*
|
|
56
|
+
* Accepted PERMANENTLY, not for a deprecation window. Every bundle emitted
|
|
57
|
+
* before 2026-09-03 carries this shape, and those bundles are evidence — a
|
|
58
|
+
* reader that stopped accepting them would destroy the record it exists to
|
|
59
|
+
* defend (DL-30). Nothing new emits it.
|
|
60
|
+
*/
|
|
61
|
+
export const ANCHOR_TRACE_BODY_TYPE_V1 = "sanning.anchor.trace/v1";
|
|
62
|
+
|
|
63
|
+
/** Every anchor-trace body this kernel verifies. Order is newest-first. */
|
|
64
|
+
export const ANCHOR_TRACE_BODY_TYPES = [
|
|
65
|
+
ANCHOR_TRACE_BODY_TYPE,
|
|
66
|
+
ANCHOR_TRACE_BODY_TYPE_V1,
|
|
67
|
+
] as const;
|
|
47
68
|
|
|
48
69
|
// The issuer-composed attested-evidence-export body (specs/evidence-export.md
|
|
49
70
|
// §2). The wrapper is unchanged (`sanning.evidence/v1`, Ed25519-signed); this body
|
|
@@ -70,20 +91,58 @@ export interface EvidenceVerdict {
|
|
|
70
91
|
[k: string]: unknown;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Where a commitment was witnessed (evidence-bundle.md §5.1.1).
|
|
96
|
+
*
|
|
97
|
+
* The same three-part shape the control plane indexes (`kind` / one promoted
|
|
98
|
+
* `id` / a typed remainder), so the signed body, the index row and the SDK
|
|
99
|
+
* artifact stop being three vocabularies for one fact.
|
|
100
|
+
*
|
|
101
|
+
* NOT ONE FIELD HERE NAMES A CHAIN. Arweave's chunk proof, data_path, offsets
|
|
102
|
+
* and packing live in `ref`, opaque to every reader above this seam. A field
|
|
103
|
+
* that would need renaming for a second witness belongs in `ref` by definition.
|
|
104
|
+
*/
|
|
105
|
+
export interface AnchorRef {
|
|
106
|
+
kind: string;
|
|
107
|
+
id: string;
|
|
108
|
+
ref?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// `sanning.anchor.trace/v2` body (§5.1). Uint8Array fields ride as lowercase hex.
|
|
74
112
|
export interface TraceCheckpoint {
|
|
75
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The checkpoint's identity and the join key, DERIVED: hex SHA-256 of the
|
|
115
|
+
* decoded `record_bytes`. Recomputed by the verifier — in v1 this was an
|
|
116
|
+
* asserted label matched against another asserted label, so a producer could
|
|
117
|
+
* relabel a checkpoint and the bundle still verified.
|
|
118
|
+
*/
|
|
119
|
+
checkpoint_ref: string;
|
|
120
|
+
/**
|
|
121
|
+
* Where it was witnessed, IF IT HAS BEEN. Optional, and absent is honest:
|
|
122
|
+
* under DL-52 a checkpoint is routinely placed before it is witnessed, and a
|
|
123
|
+
* required field that must be filled dishonestly is a defect in the format.
|
|
124
|
+
*/
|
|
125
|
+
anchor_ref?: AnchorRef;
|
|
76
126
|
envelope: Envelope;
|
|
77
127
|
record_bytes: string; // hex of JCS(checkpoint record)
|
|
78
128
|
merkle_root: string; // hex
|
|
79
129
|
}
|
|
80
130
|
|
|
131
|
+
/** The frozen `sanning.anchor.trace/v1` checkpoint (§5.1.3). Read, never written. */
|
|
132
|
+
export interface TraceCheckpointV1 {
|
|
133
|
+
tx_id: string;
|
|
134
|
+
envelope: Envelope;
|
|
135
|
+
record_bytes: string;
|
|
136
|
+
merkle_root: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
81
139
|
export interface TraceInclusion {
|
|
82
140
|
leaf_hash: string; // hex
|
|
83
141
|
leaf_index: number;
|
|
84
142
|
leaf_count: number;
|
|
85
143
|
audit_path: string[]; // hex[]
|
|
86
|
-
checkpoint_tx_id
|
|
144
|
+
/** Resolves against `checkpoints[].checkpoint_ref`. v1 spells this `checkpoint_tx_id`. */
|
|
145
|
+
checkpoint_ref: string;
|
|
87
146
|
}
|
|
88
147
|
|
|
89
148
|
export interface TraceEvent {
|
|
@@ -127,7 +186,7 @@ export type EvidenceStatus =
|
|
|
127
186
|
| "malformed"; // structurally unparseable / unknown major / body_hash mismatch
|
|
128
187
|
|
|
129
188
|
// Per-gateway on-chain outcome for one checkpoint tx (evidence-export.md §4.2).
|
|
130
|
-
// The on-chain re-fetch resolves each
|
|
189
|
+
// The on-chain re-fetch resolves each checkpoint's anchor_ref.id against EACH configured
|
|
131
190
|
// gateway and records that gateway's individual outcome:
|
|
132
191
|
// confirm — the gateway returned the tx and its bytes match the record.
|
|
133
192
|
// mismatch — the gateway returned the tx but its bytes disagree — a finding.
|
|
@@ -151,7 +210,25 @@ export interface OnChainResult {
|
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
export interface CheckpointResult {
|
|
154
|
-
|
|
213
|
+
/**
|
|
214
|
+
* The checkpoint's join key AS THE BUNDLE SPELLS IT — the derived
|
|
215
|
+
* `checkpoint_ref` for a v2 body, the legacy `tx_id` for a frozen v1 body.
|
|
216
|
+
* Never re-derived across forms: a v1 bundle's key is its `tx_id`, because
|
|
217
|
+
* that is what its own events resolve against and what an attestation
|
|
218
|
+
* written against it carries.
|
|
219
|
+
*/
|
|
220
|
+
checkpointRef: string;
|
|
221
|
+
/**
|
|
222
|
+
* Where this checkpoint was witnessed, normalised to one shape.
|
|
223
|
+
*
|
|
224
|
+
* `null` when a v2 checkpoint carries no `anchor_ref` — placed but not yet
|
|
225
|
+
* witnessed, which is a normal state and NOT a deficiency. For a frozen v1
|
|
226
|
+
* checkpoint this is synthesised as `{ kind: "arweave", id: tx_id }`, which
|
|
227
|
+
* is exactly what v1's spec said the field was ("the Arweave TX the
|
|
228
|
+
* checkpoint envelope was anchored as"). The synthesis is reader-side only
|
|
229
|
+
* and never re-enters a signed artifact or a verdict.
|
|
230
|
+
*/
|
|
231
|
+
anchorRef: AnchorRef | null;
|
|
155
232
|
// The checkpoint envelope verified (signature + payload binding to its record).
|
|
156
233
|
envelopeOk: boolean;
|
|
157
234
|
// The committed record's merkle_root equals the checkpoint's claimed merkle_root.
|
|
@@ -176,7 +253,7 @@ export interface EventResult {
|
|
|
176
253
|
payloadBindingOk: boolean | null;
|
|
177
254
|
// RFC 9162 inclusion of this event's leaf in its checkpoint's root.
|
|
178
255
|
inclusionOk: boolean;
|
|
179
|
-
// The inclusion's
|
|
256
|
+
// The inclusion's checkpoint_ref resolved to a checkpoint in the bundle.
|
|
180
257
|
checkpointBound: boolean;
|
|
181
258
|
// Raw-log (content) binding: SHA-256 of the disclosed bytes equals the
|
|
182
259
|
// committed event.content_hash (in record_bytes) — or, for a promoted
|
|
@@ -215,7 +292,12 @@ export interface EvidenceBundleResult {
|
|
|
215
292
|
// One embedded operator attestation record (§2.2 / §3). RSA-PSS-SHA-256 over
|
|
216
293
|
// JCS(payload); the operator key is the operator's Arweave RSA wallet.
|
|
217
294
|
export interface AttestationRecord {
|
|
218
|
-
|
|
295
|
+
/**
|
|
296
|
+
* The source checkpoint this attestation binds to, spelled in the SOURCE
|
|
297
|
+
* bundle's vocabulary: `checkpoint_ref` for a v2 source, the legacy `tx_id`
|
|
298
|
+
* for a frozen v1 one. Not re-derived across forms.
|
|
299
|
+
*/
|
|
300
|
+
checkpoint_ref: string;
|
|
219
301
|
payload: AttestationPayload;
|
|
220
302
|
signature_alg: string; // "rsa-pss-sha256"
|
|
221
303
|
public_key: { kty: "RSA"; n: string; e: string };
|
|
@@ -281,7 +363,7 @@ export interface VerdictAttestation {
|
|
|
281
363
|
}
|
|
282
364
|
|
|
283
365
|
export interface VerdictCheckpoint {
|
|
284
|
-
|
|
366
|
+
checkpoint_ref: string;
|
|
285
367
|
merkle_root_ok: boolean;
|
|
286
368
|
on_chain: VerdictOnChain | null;
|
|
287
369
|
attestations: VerdictAttestation[];
|
|
@@ -300,7 +382,7 @@ export interface VerdictObject {
|
|
|
300
382
|
}
|
|
301
383
|
|
|
302
384
|
export interface AttestationResult {
|
|
303
|
-
|
|
385
|
+
checkpointRef: string;
|
|
304
386
|
operator: string;
|
|
305
387
|
gateway: string | null;
|
|
306
388
|
// The RSA-PSS-SHA-256 signature over JCS(payload) verified (§5 step 6a).
|
|
@@ -310,7 +392,7 @@ export interface AttestationResult {
|
|
|
310
392
|
// payload.data_hash == the resolved source checkpoint's committed content hash
|
|
311
393
|
// (SHA-256(JCS(checkpoint.envelope))) (§5 step 6c).
|
|
312
394
|
dataHashBound: boolean;
|
|
313
|
-
//
|
|
395
|
+
// checkpoint_ref resolved to a present source checkpoint.
|
|
314
396
|
checkpointResolved: boolean;
|
|
315
397
|
level: number | null;
|
|
316
398
|
// subject_ref (§3.2) tri-state: null absent/undetermined, true/false only
|
|
@@ -340,7 +422,7 @@ export interface ExportResult {
|
|
|
340
422
|
}
|
|
341
423
|
|
|
342
424
|
export interface VerifyEvidenceOptions {
|
|
343
|
-
// Gateways to re-fetch each
|
|
425
|
+
// Gateways to re-fetch each anchor_ref.id from (first responsive wins).
|
|
344
426
|
// When supplied, the on-chain bytes MUST byte-match the bundle's checkpoint
|
|
345
427
|
// envelope, proving the proof is anchored, not just locally asserted.
|
|
346
428
|
gateways?: string[];
|
|
@@ -349,7 +431,15 @@ export interface VerifyEvidenceOptions {
|
|
|
349
431
|
// Uint8Array is used as-is; a string is parsed as lowercase hex. In-body
|
|
350
432
|
// `events[].content` (signed) takes precedence over this side input; if both
|
|
351
433
|
// are present for an event and disagree, the event fails (§5.1).
|
|
352
|
-
|
|
434
|
+
//
|
|
435
|
+
// A `Map` is the SAFE shape and the one to prefer — it is what
|
|
436
|
+
// `disclosedContent` hands back, so our output feeds straight back in, and a
|
|
437
|
+
// Map keys on the string itself: no prototype to inherit from and no
|
|
438
|
+
// `__proto__` write that silently lands somewhere else. The plain `Record` is
|
|
439
|
+
// still accepted (it was the original signature and remains supported), and
|
|
440
|
+
// the lookup guards it — see `lookupSideContent`. Widening is additive: every
|
|
441
|
+
// existing caller passing a Record keeps working unchanged.
|
|
442
|
+
content?: Record<string, Uint8Array | string> | ReadonlyMap<string, Uint8Array | string>;
|
|
353
443
|
// Injectable fetch (Node >=18 global `fetch` by default) — test seam.
|
|
354
444
|
fetchImpl?: typeof fetch;
|
|
355
445
|
}
|
|
@@ -482,13 +572,13 @@ export async function verifyEvidenceBundle(
|
|
|
482
572
|
return verifyExportBody(b, specVersionOk, assertedStatus, errors, options);
|
|
483
573
|
}
|
|
484
574
|
|
|
485
|
-
if (bodyType !== ANCHOR_TRACE_BODY_TYPE) {
|
|
575
|
+
if (bodyType !== ANCHOR_TRACE_BODY_TYPE && bodyType !== ANCHOR_TRACE_BODY_TYPE_V1) {
|
|
486
576
|
// The wrapper is authentic but we don't know this body's structure. Per
|
|
487
577
|
// wrap-don't-merge, that is "partial" (cryptographically sound wrapper,
|
|
488
578
|
// body verdict undetermined) — not a failure.
|
|
489
579
|
errors.push(
|
|
490
580
|
`body_type ${JSON.stringify(bodyType)} is not verifiable by this kernel ` +
|
|
491
|
-
`(only ${
|
|
581
|
+
`(only ${ANCHOR_TRACE_BODY_TYPES.join(", ")}); wrapper verified, body undetermined`,
|
|
492
582
|
);
|
|
493
583
|
return {
|
|
494
584
|
status: "partial",
|
|
@@ -507,6 +597,158 @@ export async function verifyEvidenceBundle(
|
|
|
507
597
|
return verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, errors, options);
|
|
508
598
|
}
|
|
509
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Index a source bundle's checkpoints by the key its attestations will use.
|
|
602
|
+
*
|
|
603
|
+
* The attestation join key follows the SOURCE bundle's own vocabulary — a
|
|
604
|
+
* v2 source is keyed by `checkpoint_ref`, a frozen v1 source by `tx_id`. The
|
|
605
|
+
* alternative (always deriving a v2-style key) would silently un-resolve every
|
|
606
|
+
* attestation ever written against a v1 export: `data_hash_bound` would go
|
|
607
|
+
* false, the export would stop reaching `verified`, and nothing would say why.
|
|
608
|
+
*/
|
|
609
|
+
async function indexSourceCheckpoints(
|
|
610
|
+
bundle: EvidenceBundle,
|
|
611
|
+
): Promise<Map<string, AnyTraceCheckpoint>> {
|
|
612
|
+
const out = new Map<string, AnyTraceCheckpoint>();
|
|
613
|
+
const sb = bundle.body as AnchorTraceBody | undefined;
|
|
614
|
+
const isV1 = bundle.body_type === ANCHOR_TRACE_BODY_TYPE_V1;
|
|
615
|
+
if (!sb || typeof sb !== "object" || !Array.isArray(sb.checkpoints)) return out;
|
|
616
|
+
for (const cp of sb.checkpoints as AnyTraceCheckpoint[]) {
|
|
617
|
+
if (!cp || typeof cp !== "object") continue;
|
|
618
|
+
let recordBytes: Uint8Array | undefined;
|
|
619
|
+
try {
|
|
620
|
+
if (typeof cp.record_bytes === "string") recordBytes = hexToBytes(cp.record_bytes);
|
|
621
|
+
} catch {
|
|
622
|
+
recordBytes = undefined;
|
|
623
|
+
}
|
|
624
|
+
const keyed = await resolveCheckpointKey(cp, recordBytes, isV1);
|
|
625
|
+
if (keyed.key !== null && !out.has(keyed.key)) out.set(keyed.key, cp);
|
|
626
|
+
}
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Either shape of checkpoint, before the form is resolved. */
|
|
631
|
+
type AnyTraceCheckpoint = Partial<TraceCheckpoint> & Partial<TraceCheckpointV1>;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Resolve a checkpoint's join key and its witness reference, for whichever
|
|
635
|
+
* body form we are reading.
|
|
636
|
+
*
|
|
637
|
+
* ── The rule that matters, and why it is a reject rather than a preference ──
|
|
638
|
+
*
|
|
639
|
+
* A checkpoint carrying BOTH vocabularies is two answers to one question
|
|
640
|
+
* inside one signature. A reader that silently prefers one gives a different
|
|
641
|
+
* answer from a reader that prefers the other, over the same signed bytes —
|
|
642
|
+
* and both look like they verified. So a v2 body carrying `tx_id`, or a v1
|
|
643
|
+
* body carrying `anchor_ref`/`checkpoint_ref`, is rejected rather than
|
|
644
|
+
* resolved. This is cheap to enforce here and impossible to enforce later.
|
|
645
|
+
*
|
|
646
|
+
* ── Why v2's key is recomputed ──
|
|
647
|
+
*
|
|
648
|
+
* In v1 the key was an asserted label matched against another asserted label;
|
|
649
|
+
* nothing tied either to the bytes, so relabelling a checkpoint and every
|
|
650
|
+
* event pointing at it left a bundle that still verified. In v2 the key IS the
|
|
651
|
+
* bytes, so the same relabel fails here. v1's behaviour is preserved exactly
|
|
652
|
+
* as it was — deliberately, because those bundles are already signed and
|
|
653
|
+
* re-interpreting them is not backwards compatibility.
|
|
654
|
+
*/
|
|
655
|
+
async function resolveCheckpointKey(
|
|
656
|
+
cp: AnyTraceCheckpoint,
|
|
657
|
+
recordBytes: Uint8Array | undefined,
|
|
658
|
+
isV1: boolean,
|
|
659
|
+
): Promise<{ key: string | null; anchorRef: AnchorRef | null; errors: string[] }> {
|
|
660
|
+
const errors: string[] = [];
|
|
661
|
+
|
|
662
|
+
if (isV1) {
|
|
663
|
+
if (cp.checkpoint_ref !== undefined || cp.anchor_ref !== undefined) {
|
|
664
|
+
errors.push(
|
|
665
|
+
"sanning.anchor.trace/v1 checkpoint carries v2 fields (checkpoint_ref/anchor_ref) — " +
|
|
666
|
+
"one bundle speaks one vocabulary",
|
|
667
|
+
);
|
|
668
|
+
return { key: null, anchorRef: null, errors };
|
|
669
|
+
}
|
|
670
|
+
if (typeof cp.tx_id !== "string" || cp.tx_id === "") {
|
|
671
|
+
errors.push("checkpoint missing tx_id");
|
|
672
|
+
return { key: null, anchorRef: null, errors };
|
|
673
|
+
}
|
|
674
|
+
// v1's tx_id IS an Arweave transaction — its own spec said so. Surfacing it
|
|
675
|
+
// through the same shape as v2 gives ONE reader-side vocabulary without
|
|
676
|
+
// touching a byte of the signed bundle.
|
|
677
|
+
return { key: cp.tx_id, anchorRef: { kind: "arweave", id: cp.tx_id }, errors };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (cp.tx_id !== undefined) {
|
|
681
|
+
errors.push(
|
|
682
|
+
"sanning.anchor.trace/v2 checkpoint carries legacy tx_id — one bundle speaks one vocabulary",
|
|
683
|
+
);
|
|
684
|
+
return { key: null, anchorRef: null, errors };
|
|
685
|
+
}
|
|
686
|
+
if (typeof cp.checkpoint_ref !== "string" || cp.checkpoint_ref === "") {
|
|
687
|
+
errors.push("checkpoint missing checkpoint_ref");
|
|
688
|
+
return { key: null, anchorRef: null, errors };
|
|
689
|
+
}
|
|
690
|
+
if (recordBytes === undefined) {
|
|
691
|
+
// Without the bytes the key cannot be recomputed, so it cannot be trusted.
|
|
692
|
+
errors.push("checkpoint_ref cannot be verified — record_bytes missing or not hex");
|
|
693
|
+
return { key: null, anchorRef: null, errors };
|
|
694
|
+
}
|
|
695
|
+
const derived = await sha256Hex(recordBytes);
|
|
696
|
+
if (derived !== cp.checkpoint_ref.toLowerCase()) {
|
|
697
|
+
errors.push(
|
|
698
|
+
`checkpoint_ref does not match SHA-256(record_bytes): claimed=${cp.checkpoint_ref} derived=${derived}`,
|
|
699
|
+
);
|
|
700
|
+
return { key: null, anchorRef: null, errors };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const anchorRef = parseAnchorRef(cp.anchor_ref, errors);
|
|
704
|
+
return { key: derived, anchorRef, errors };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Validate an optional `anchor_ref` (§5.1.1).
|
|
709
|
+
*
|
|
710
|
+
* ── An unrecognised `kind` is NOT a failure, and that is the point ──
|
|
711
|
+
*
|
|
712
|
+
* The offline verdict — signatures, payload binding, Merkle inclusion — is
|
|
713
|
+
* computed with no witness access at all. A reader that cannot resolve a
|
|
714
|
+
* witness has therefore learned NOTHING about whether the evidence was
|
|
715
|
+
* altered, and must not say otherwise. The shipped issuer did the opposite:
|
|
716
|
+
* an unrecognised signature type returned `false` rather than `null`, mapped
|
|
717
|
+
* to `tampered`, and was cached as permanent — a false accusation, produced
|
|
718
|
+
* by a coverage gap, for a company whose product is telling people whether
|
|
719
|
+
* records were altered.
|
|
720
|
+
*
|
|
721
|
+
* So this validates the SHAPE and never the vocabulary. A malformed
|
|
722
|
+
* `anchor_ref` is a defect in the bundle; an unknown `kind` is a defect in
|
|
723
|
+
* the reader, and only the first is the bundle's fault.
|
|
724
|
+
*/
|
|
725
|
+
function parseAnchorRef(raw: unknown, errors: string[]): AnchorRef | null {
|
|
726
|
+
if (raw === undefined || raw === null) return null; // placed, not yet witnessed
|
|
727
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
728
|
+
errors.push("anchor_ref must be an object");
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const o = raw as Record<string, unknown>;
|
|
732
|
+
if (typeof o.kind !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(o.kind)) {
|
|
733
|
+
errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind)}`);
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
if (typeof o.id !== "string" || o.id === "") {
|
|
737
|
+
errors.push("anchor_ref.id is missing or empty");
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
const ref = o.ref;
|
|
741
|
+
if (ref !== undefined && (typeof ref !== "object" || ref === null || Array.isArray(ref))) {
|
|
742
|
+
errors.push("anchor_ref.ref must be an object when present");
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
kind: o.kind,
|
|
747
|
+
id: o.id,
|
|
748
|
+
...(ref === undefined ? {} : { ref: ref as Record<string, unknown> }),
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
510
752
|
async function verifyAnchorTrace(
|
|
511
753
|
b: EvidenceBundle,
|
|
512
754
|
bodyType: string,
|
|
@@ -516,40 +758,55 @@ async function verifyAnchorTrace(
|
|
|
516
758
|
options: VerifyEvidenceOptions,
|
|
517
759
|
): Promise<EvidenceBundleResult> {
|
|
518
760
|
const body = b.body as AnchorTraceBody;
|
|
761
|
+
const isV1 = bodyType === ANCHOR_TRACE_BODY_TYPE_V1;
|
|
519
762
|
if (!Array.isArray(body.checkpoints) || !Array.isArray(body.events)) {
|
|
520
|
-
return malformed(
|
|
763
|
+
return malformed(`${bodyType} body must have checkpoints[] and events[]`, true);
|
|
521
764
|
}
|
|
522
765
|
|
|
523
766
|
const gateways = options.gateways ?? [];
|
|
524
767
|
const onChainChecked = gateways.length > 0;
|
|
525
768
|
|
|
526
769
|
// --- Step 3: per-checkpoint -------------------------------------------------
|
|
527
|
-
|
|
770
|
+
//
|
|
771
|
+
// ── Why the join key is resolved BEFORE anything else ──
|
|
772
|
+
//
|
|
773
|
+
// `byKey` is what binds an event to the root its inclusion proof is checked
|
|
774
|
+
// against. Get it wrong and an event proves inclusion in a tree it was never
|
|
775
|
+
// in — the one failure in this file that produces a WRONG VERIFIED, not a
|
|
776
|
+
// failed verify. Everything below is downstream of it.
|
|
777
|
+
const byKey = new Map<string, AnyTraceCheckpoint>();
|
|
778
|
+
const anchorByKey = new Map<string, AnchorRef | null>();
|
|
528
779
|
const checkpointResults: CheckpointResult[] = [];
|
|
529
|
-
for (const cp of body.checkpoints) {
|
|
780
|
+
for (const cp of body.checkpoints as AnyTraceCheckpoint[]) {
|
|
530
781
|
const cpErrors: string[] = [];
|
|
531
782
|
let envelopeOk = false;
|
|
532
783
|
let merkleRootOk = false;
|
|
533
784
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
} else if (byTxId.has(cp.tx_id)) {
|
|
537
|
-
cpErrors.push(`duplicate checkpoint tx_id ${cp.tx_id}`);
|
|
538
|
-
} else {
|
|
539
|
-
byTxId.set(cp.tx_id, cp);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Verify the checkpoint envelope against its committed record (external
|
|
543
|
-
// commitment — the record is the payload these external envelopes bind to).
|
|
785
|
+
// Decode record_bytes first: in v2 the join key is DERIVED from it, so it
|
|
786
|
+
// is an input to identity, not just to the payload binding below.
|
|
544
787
|
let recordBytes: Uint8Array | undefined;
|
|
545
788
|
try {
|
|
546
789
|
if (typeof cp.record_bytes === "string") recordBytes = hexToBytes(cp.record_bytes);
|
|
547
790
|
} catch (e) {
|
|
548
791
|
cpErrors.push(`checkpoint record_bytes is not hex: ${stringifyErr(e)}`);
|
|
549
792
|
}
|
|
793
|
+
|
|
794
|
+
const keyed = await resolveCheckpointKey(cp, recordBytes, isV1);
|
|
795
|
+
cpErrors.push(...keyed.errors);
|
|
796
|
+
if (keyed.key !== null) {
|
|
797
|
+
if (byKey.has(keyed.key)) {
|
|
798
|
+
cpErrors.push(`duplicate checkpoint ${isV1 ? "tx_id" : "checkpoint_ref"} ${keyed.key}`);
|
|
799
|
+
} else {
|
|
800
|
+
byKey.set(keyed.key, cp);
|
|
801
|
+
anchorByKey.set(keyed.key, keyed.anchorRef);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Verify the checkpoint envelope against its committed record (external
|
|
806
|
+
// commitment — the record is the payload these external envelopes bind to).
|
|
550
807
|
try {
|
|
551
808
|
const res = await verifyEnvelope(
|
|
552
|
-
cp.envelope,
|
|
809
|
+
cp.envelope as Envelope,
|
|
553
810
|
recordBytes !== undefined ? { payloadBytes: recordBytes } : {},
|
|
554
811
|
);
|
|
555
812
|
// A checkpoint must fully bind: signature + the committed record matching
|
|
@@ -583,12 +840,19 @@ async function verifyAnchorTrace(
|
|
|
583
840
|
}
|
|
584
841
|
|
|
585
842
|
checkpointResults.push({
|
|
586
|
-
|
|
843
|
+
checkpointRef: keyed.key ?? "",
|
|
844
|
+
anchorRef: keyed.anchorRef,
|
|
587
845
|
envelopeOk,
|
|
588
846
|
merkleRootOk,
|
|
589
847
|
onChainOk: null, // filled in step 5 if a gateway re-fetch was requested
|
|
590
848
|
onChain: null, // per-gateway (§4.2), filled in step 5
|
|
591
|
-
|
|
849
|
+
// `keyed.errors` is load-bearing here, not decorative. A structurally
|
|
850
|
+
// invalid anchor_ref rides INSIDE the signature, so it is a defect we
|
|
851
|
+
// can see rather than a coverage gap we cannot — and this kernel's
|
|
852
|
+
// standing rule is that malformed is a failure while absent is
|
|
853
|
+
// undetermined (the same rule disclosed `content` follows). Without
|
|
854
|
+
// this term the vector `trace-v2-malformed-anchor-ref` verified GREEN.
|
|
855
|
+
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0,
|
|
592
856
|
errors: cpErrors,
|
|
593
857
|
});
|
|
594
858
|
}
|
|
@@ -622,16 +886,34 @@ async function verifyAnchorTrace(
|
|
|
622
886
|
|
|
623
887
|
// Bind the event to exactly one checkpoint in the bundle, then verify
|
|
624
888
|
// RFC 9162 inclusion against THAT checkpoint's merkle_root.
|
|
625
|
-
const incl = ev.inclusion;
|
|
626
|
-
|
|
627
|
-
|
|
889
|
+
const incl = ev.inclusion as (TraceInclusion & { checkpoint_tx_id?: unknown }) | undefined;
|
|
890
|
+
// The join key is spelled differently in the two forms and MUST NOT be read
|
|
891
|
+
// from the other one: an inclusion carrying v1's spelling inside a v2 body
|
|
892
|
+
// is a mixed bundle, and resolving it anyway is how an event silently binds
|
|
893
|
+
// to a checkpoint nobody meant.
|
|
894
|
+
const keyField = isV1 ? "checkpoint_tx_id" : "checkpoint_ref";
|
|
895
|
+
const wrongField = isV1 ? "checkpoint_ref" : "checkpoint_tx_id";
|
|
896
|
+
const inclAsRecord = incl as unknown as Record<string, unknown> | undefined;
|
|
897
|
+
const rawKey = inclAsRecord ? inclAsRecord[keyField] : undefined;
|
|
898
|
+
const strayKey = inclAsRecord ? inclAsRecord[wrongField] : undefined;
|
|
899
|
+
let cp: AnyTraceCheckpoint | undefined;
|
|
628
900
|
if (!incl || typeof incl !== "object") {
|
|
629
901
|
evErrors.push("event missing inclusion proof");
|
|
630
|
-
} else if (
|
|
902
|
+
} else if (strayKey !== undefined) {
|
|
631
903
|
evErrors.push(
|
|
632
|
-
`event inclusion
|
|
904
|
+
`event inclusion carries ${wrongField} in a ${bodyType} body — one bundle speaks one vocabulary`,
|
|
633
905
|
);
|
|
906
|
+
} else if (typeof rawKey !== "string") {
|
|
907
|
+
evErrors.push(`event inclusion missing ${keyField}`);
|
|
908
|
+
} else {
|
|
909
|
+
cp = byKey.get(rawKey);
|
|
910
|
+
if (cp === undefined) {
|
|
911
|
+
evErrors.push(
|
|
912
|
+
`event inclusion.${keyField} ${JSON.stringify(rawKey)} resolves to no checkpoint`,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
634
915
|
}
|
|
916
|
+
const checkpointBound = cp !== undefined;
|
|
635
917
|
|
|
636
918
|
let inclusionOk = false;
|
|
637
919
|
if (incl && checkpointBound && cp) {
|
|
@@ -641,7 +923,7 @@ async function verifyAnchorTrace(
|
|
|
641
923
|
incl.leaf_index,
|
|
642
924
|
incl.leaf_count,
|
|
643
925
|
(incl.audit_path ?? []).map((h) => hexToBytes(h)),
|
|
644
|
-
hexToBytes(cp.merkle_root),
|
|
926
|
+
hexToBytes(cp.merkle_root ?? ""),
|
|
645
927
|
);
|
|
646
928
|
if (!inclusionOk) evErrors.push("RFC 9162 inclusion proof did not reconstruct the checkpoint root");
|
|
647
929
|
|
|
@@ -761,9 +1043,26 @@ async function verifyAnchorTrace(
|
|
|
761
1043
|
errors.push("on-chain re-fetch requested but no fetch implementation is available");
|
|
762
1044
|
} else {
|
|
763
1045
|
for (const cpr of checkpointResults) {
|
|
764
|
-
const cp =
|
|
1046
|
+
const cp = byKey.get(cpr.checkpointRef);
|
|
765
1047
|
if (!cp) continue;
|
|
766
|
-
|
|
1048
|
+
// A checkpoint with no anchor_ref has NOTHING to re-fetch. Under DL-52
|
|
1049
|
+
// that is the normal state inside a stamp interval — placed, readable,
|
|
1050
|
+
// not yet witnessed — so it reports as unreachable (which downgrades
|
|
1051
|
+
// the run to partial) and NEVER as a mismatch. Treating "we never
|
|
1052
|
+
// anchored it yet" as "the chain disagrees" would be the same false
|
|
1053
|
+
// accusation this whole change is removing.
|
|
1054
|
+
if (cpr.anchorRef === null) {
|
|
1055
|
+
gatewayUnavailable = true;
|
|
1056
|
+
cpr.onChain = {
|
|
1057
|
+
perGateway: gateways.map((gw) => ({ gateway: gw, outcome: "unreachable" as const })),
|
|
1058
|
+
rollup: "unreachable",
|
|
1059
|
+
onChainOk: null,
|
|
1060
|
+
};
|
|
1061
|
+
cpr.onChainOk = null;
|
|
1062
|
+
cpr.errors.push("checkpoint has no anchor_ref — not yet witnessed, nothing to re-fetch");
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const outcome = await refetchCheckpoint(cp, cpr.anchorRef, gateways, fetchImpl);
|
|
767
1066
|
cpr.onChain = outcome.onChain;
|
|
768
1067
|
cpr.onChainOk = outcome.onChain.onChainOk;
|
|
769
1068
|
if (outcome.unavailable) gatewayUnavailable = true;
|
|
@@ -910,7 +1209,7 @@ export async function disclosedContent(
|
|
|
910
1209
|
const traceEvents = resolveTraceEvents(bundle);
|
|
911
1210
|
if (traceEvents === undefined) {
|
|
912
1211
|
errors.push(
|
|
913
|
-
`no ${
|
|
1212
|
+
`no ${ANCHOR_TRACE_BODY_TYPES.join(" / ")} body to materialize content from ` +
|
|
914
1213
|
`(body_type ${JSON.stringify(verification.bodyType)})`,
|
|
915
1214
|
);
|
|
916
1215
|
return empty();
|
|
@@ -1013,7 +1312,7 @@ function resolveTraceEvents(bundle: unknown): TraceEvent[] | undefined {
|
|
|
1013
1312
|
if (cur === null || typeof cur !== "object" || Array.isArray(cur)) return undefined;
|
|
1014
1313
|
const b = cur as EvidenceBundle;
|
|
1015
1314
|
const bodyType = typeof b.body_type === "string" ? b.body_type : null;
|
|
1016
|
-
if (bodyType === ANCHOR_TRACE_BODY_TYPE) {
|
|
1315
|
+
if (bodyType === ANCHOR_TRACE_BODY_TYPE || bodyType === ANCHOR_TRACE_BODY_TYPE_V1) {
|
|
1017
1316
|
const body = b.body as AnchorTraceBody | null | undefined;
|
|
1018
1317
|
if (body === null || typeof body !== "object" || !Array.isArray(body.events)) return undefined;
|
|
1019
1318
|
return body.events;
|
|
@@ -1118,14 +1417,8 @@ async function verifyExportBody(
|
|
|
1118
1417
|
// content_ok, and the optional per-gateway on-chain re-fetch, §4.2 / step 7).
|
|
1119
1418
|
const sourceResult = await verifyEvidenceBundle(inline, options);
|
|
1120
1419
|
|
|
1121
|
-
//
|
|
1122
|
-
const sourceCheckpoints =
|
|
1123
|
-
const sb = inline.body as AnchorTraceBody | undefined;
|
|
1124
|
-
if (sb && typeof sb === "object" && Array.isArray(sb.checkpoints)) {
|
|
1125
|
-
for (const cp of sb.checkpoints) {
|
|
1126
|
-
if (cp && typeof cp.tx_id === "string") sourceCheckpoints.set(cp.tx_id, cp);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1420
|
+
// Attestation data_hash binding (§5 6c).
|
|
1421
|
+
const sourceCheckpoints = await indexSourceCheckpoints(inline);
|
|
1129
1422
|
|
|
1130
1423
|
// --- Step 6: embedded attestation records ---------------------------------
|
|
1131
1424
|
const attestations: AttestationResult[] = [];
|
|
@@ -1257,15 +1550,9 @@ export async function recomputeExportVerdict(
|
|
|
1257
1550
|
): Promise<ComposeVerdictResult> {
|
|
1258
1551
|
const sourceResult = await verifyEvidenceBundle(sourceBundle, options);
|
|
1259
1552
|
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1262
|
-
const sourceCheckpoints =
|
|
1263
|
-
const sb = sourceBundle.body as AnchorTraceBody | undefined;
|
|
1264
|
-
if (sb && typeof sb === "object" && Array.isArray(sb.checkpoints)) {
|
|
1265
|
-
for (const cp of sb.checkpoints) {
|
|
1266
|
-
if (cp && typeof cp.tx_id === "string") sourceCheckpoints.set(cp.tx_id, cp);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1553
|
+
// Attestation data_hash binding (§5 6c) — the SAME index construction as
|
|
1554
|
+
// verifyExportBody, deliberately shared rather than restated.
|
|
1555
|
+
const sourceCheckpoints = await indexSourceCheckpoints(sourceBundle);
|
|
1269
1556
|
|
|
1270
1557
|
const attestations: AttestationResult[] = [];
|
|
1271
1558
|
for (const rec of attestationRecords) {
|
|
@@ -1309,12 +1596,12 @@ export async function recomputeExportVerdict(
|
|
|
1309
1596
|
// undetermined-tolerant and never gates.
|
|
1310
1597
|
async function verifyAttestation(
|
|
1311
1598
|
rec: AttestationRecord,
|
|
1312
|
-
sourceCheckpoints: Map<string,
|
|
1599
|
+
sourceCheckpoints: Map<string, AnyTraceCheckpoint>,
|
|
1313
1600
|
): Promise<{ result: AttestationResult; malformed?: string }> {
|
|
1314
1601
|
const errs: string[] = [];
|
|
1315
1602
|
const result: AttestationResult = {
|
|
1316
|
-
|
|
1317
|
-
rec && typeof rec.
|
|
1603
|
+
checkpointRef:
|
|
1604
|
+
rec && typeof rec.checkpoint_ref === "string" ? rec.checkpoint_ref : "",
|
|
1318
1605
|
operator: "",
|
|
1319
1606
|
gateway: null,
|
|
1320
1607
|
signatureOk: false,
|
|
@@ -1392,11 +1679,11 @@ async function verifyAttestation(
|
|
|
1392
1679
|
// uploaded JCS(envelope) bytes (the same bytes the on-chain re-fetch compares
|
|
1393
1680
|
// against, §4.2), so the checkpoint's committed content hash is
|
|
1394
1681
|
// SHA-256(JCS(checkpoint.envelope)).
|
|
1395
|
-
const cp = sourceCheckpoints.get(result.
|
|
1682
|
+
const cp = sourceCheckpoints.get(result.checkpointRef);
|
|
1396
1683
|
result.checkpointResolved = cp !== undefined;
|
|
1397
1684
|
if (!cp) {
|
|
1398
1685
|
errs.push(
|
|
1399
|
-
`attestation
|
|
1686
|
+
`attestation checkpoint_ref ${JSON.stringify(result.checkpointRef)} resolves to no source checkpoint`,
|
|
1400
1687
|
);
|
|
1401
1688
|
} else {
|
|
1402
1689
|
try {
|
|
@@ -1455,7 +1742,7 @@ function buildVerdictObject(
|
|
|
1455
1742
|
): VerdictObject {
|
|
1456
1743
|
const attByCheckpoint = new Map<string, VerdictAttestation[]>();
|
|
1457
1744
|
for (const a of attestations) {
|
|
1458
|
-
const list = attByCheckpoint.get(a.
|
|
1745
|
+
const list = attByCheckpoint.get(a.checkpointRef) ?? [];
|
|
1459
1746
|
list.push({
|
|
1460
1747
|
operator: a.operator,
|
|
1461
1748
|
...(a.gateway ? { gateway: a.gateway } : {}),
|
|
@@ -1465,7 +1752,7 @@ function buildVerdictObject(
|
|
|
1465
1752
|
...(a.level !== null ? { level: a.level } : {}),
|
|
1466
1753
|
subject_ref_ok: a.subjectRefOk,
|
|
1467
1754
|
});
|
|
1468
|
-
attByCheckpoint.set(a.
|
|
1755
|
+
attByCheckpoint.set(a.checkpointRef, list);
|
|
1469
1756
|
}
|
|
1470
1757
|
|
|
1471
1758
|
const events: VerdictEvent[] = source.events.map((e) => ({
|
|
@@ -1478,10 +1765,10 @@ function buildVerdictObject(
|
|
|
1478
1765
|
}));
|
|
1479
1766
|
|
|
1480
1767
|
const checkpoints: VerdictCheckpoint[] = source.checkpoints.map((c) => ({
|
|
1481
|
-
|
|
1768
|
+
checkpoint_ref: c.checkpointRef,
|
|
1482
1769
|
merkle_root_ok: c.merkleRootOk,
|
|
1483
1770
|
on_chain: c.onChain ? toVerdictOnChain(c.onChain) : null,
|
|
1484
|
-
attestations: attByCheckpoint.get(c.
|
|
1771
|
+
attestations: attByCheckpoint.get(c.checkpointRef) ?? [],
|
|
1485
1772
|
}));
|
|
1486
1773
|
|
|
1487
1774
|
let verified = 0;
|
|
@@ -1550,7 +1837,7 @@ function projectVerdictForAgreement(v: unknown): unknown {
|
|
|
1550
1837
|
const cp = (c ?? {}) as VerdictCheckpoint;
|
|
1551
1838
|
const atts = Array.isArray(cp.attestations) ? cp.attestations : [];
|
|
1552
1839
|
return {
|
|
1553
|
-
|
|
1840
|
+
checkpoint_ref: cp.checkpoint_ref ?? null,
|
|
1554
1841
|
merkle_root_ok: cp.merkle_root_ok ?? null,
|
|
1555
1842
|
// on_chain DELIBERATELY OMITTED — environment/time-dependent (see step 5).
|
|
1556
1843
|
attestations: atts.map((a) => {
|
|
@@ -1588,7 +1875,8 @@ function summarizeExport(
|
|
|
1588
1875
|
// gateways is preserved; the derived collapsed `onChainOk` reproduces the old
|
|
1589
1876
|
// single-boolean exactly for a single gateway.
|
|
1590
1877
|
async function refetchCheckpoint(
|
|
1591
|
-
cp:
|
|
1878
|
+
cp: AnyTraceCheckpoint,
|
|
1879
|
+
anchorRef: AnchorRef,
|
|
1592
1880
|
gateways: string[],
|
|
1593
1881
|
fetchImpl: typeof fetch,
|
|
1594
1882
|
): Promise<{ onChain: OnChainResult; unavailable: boolean; error?: string }> {
|
|
@@ -1603,15 +1891,18 @@ async function refetchCheckpoint(
|
|
|
1603
1891
|
|
|
1604
1892
|
const perGateway: PerGatewayOutcome[] = [];
|
|
1605
1893
|
for (const gw of gateways) {
|
|
1606
|
-
perGateway.push({
|
|
1894
|
+
perGateway.push({
|
|
1895
|
+
gateway: gw,
|
|
1896
|
+
outcome: await probeGateway(anchorRef, gw, expectedHex, fetchImpl),
|
|
1897
|
+
});
|
|
1607
1898
|
}
|
|
1608
1899
|
|
|
1609
1900
|
const onChain = rollupOnChain(perGateway);
|
|
1610
1901
|
let error: string | undefined;
|
|
1611
1902
|
if (onChain.rollup === "mismatch") {
|
|
1612
|
-
error = `on-chain bytes at ${
|
|
1903
|
+
error = `on-chain bytes at ${anchorRef.id} do not match the bundle's checkpoint envelope`;
|
|
1613
1904
|
} else if (onChain.rollup === "unreachable") {
|
|
1614
|
-
error = `checkpoint ${
|
|
1905
|
+
error = `checkpoint ${anchorRef.id} unreachable on all gateways`;
|
|
1615
1906
|
}
|
|
1616
1907
|
return { onChain, unavailable: onChain.rollup === "unreachable", error };
|
|
1617
1908
|
}
|
|
@@ -1619,12 +1910,12 @@ async function refetchCheckpoint(
|
|
|
1619
1910
|
// One gateway's outcome for one checkpoint tx: confirm (bytes match), mismatch
|
|
1620
1911
|
// (bytes disagree), or unreachable (no response / !ok / network error).
|
|
1621
1912
|
async function probeGateway(
|
|
1622
|
-
|
|
1913
|
+
anchorRef: AnchorRef,
|
|
1623
1914
|
gateway: string,
|
|
1624
1915
|
expectedHex: string | undefined,
|
|
1625
1916
|
fetchImpl: typeof fetch,
|
|
1626
1917
|
): Promise<OnChainOutcome> {
|
|
1627
|
-
const url = `${gateway.replace(/\/+$/, "")}/${
|
|
1918
|
+
const url = `${gateway.replace(/\/+$/, "")}/${anchorRef.id}`;
|
|
1628
1919
|
let resp: Awaited<ReturnType<typeof fetch>>;
|
|
1629
1920
|
try {
|
|
1630
1921
|
resp = await fetchImpl(url);
|
|
@@ -1691,6 +1982,15 @@ interface DisclosedSelection {
|
|
|
1691
1982
|
errors: string[];
|
|
1692
1983
|
}
|
|
1693
1984
|
|
|
1985
|
+
// The side-input presence rule lives in `side-content.ts`, stated ONCE.
|
|
1986
|
+
//
|
|
1987
|
+
// It used to live here as a private `lookupSideContent`, and `grading.ts`
|
|
1988
|
+
// carried a second, laxer answer to the same question (`hasEntry`: own +
|
|
1989
|
+
// enumerable, nothing more). On one and the same content map the grader said
|
|
1990
|
+
// PRESENT while this verifier said nothing was offered, and `gradePack` then
|
|
1991
|
+
// returned `incomplete` with ZERO causes. Read `side-content.ts` for the rule
|
|
1992
|
+
// and for what each clause of it cost.
|
|
1993
|
+
|
|
1694
1994
|
function selectDisclosedBytes(
|
|
1695
1995
|
ev: TraceEvent,
|
|
1696
1996
|
eventId: string,
|
|
@@ -1706,17 +2006,31 @@ function selectDisclosedBytes(
|
|
|
1706
2006
|
}
|
|
1707
2007
|
}
|
|
1708
2008
|
let side: Uint8Array | undefined;
|
|
1709
|
-
const
|
|
1710
|
-
if (
|
|
1711
|
-
side =
|
|
1712
|
-
} else if (typeof
|
|
2009
|
+
const sideEntry: SideEntry = sideContentEntry(sideContent, eventId);
|
|
2010
|
+
if (sideEntry.value instanceof Uint8Array) {
|
|
2011
|
+
side = sideEntry.value;
|
|
2012
|
+
} else if (typeof sideEntry.value === "string") {
|
|
1713
2013
|
try {
|
|
1714
|
-
side = hexToBytes(
|
|
2014
|
+
side = hexToBytes(sideEntry.value);
|
|
1715
2015
|
} catch (e) {
|
|
1716
2016
|
errors.push(`supplied content for ${eventId} is not hex: ${stringifyErr(e)}`);
|
|
1717
2017
|
}
|
|
1718
2018
|
}
|
|
1719
|
-
|
|
2019
|
+
// `offered` comes from the SAME test that produced the value, never from
|
|
2020
|
+
// `value !== undefined` — that is the conflation this whole guard exists to
|
|
2021
|
+
// remove, and it was removed from ONE of the two operands. The left one
|
|
2022
|
+
// carried it unchanged: an in-body `"content": null` is `!== undefined`, so
|
|
2023
|
+
// the event was marked offered while no bytes were selected, and the kernel
|
|
2024
|
+
// split against itself — `verifyEvidenceBundle` VERIFIED, `disclosedContent`
|
|
2025
|
+
// FAILED, on one bundle. `null` is how an optional field's absence
|
|
2026
|
+
// serializes; `inBodyContentOffered` states that once, beside the side-input
|
|
2027
|
+
// rule it mirrors.
|
|
2028
|
+
return {
|
|
2029
|
+
inBody,
|
|
2030
|
+
side,
|
|
2031
|
+
offered: inBodyContentOffered(ev.content) || sideEntry.offered,
|
|
2032
|
+
errors,
|
|
2033
|
+
};
|
|
1720
2034
|
}
|
|
1721
2035
|
|
|
1722
2036
|
function toHex(bytes: Uint8Array): string {
|