@sanning/proof 0.11.1 → 0.13.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 +43 -8
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +349 -0
- package/dist/case.d.ts +199 -43
- package/dist/case.js +522 -146
- package/dist/cli.d.ts +132 -1
- package/dist/cli.js +1140 -96
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.js +134 -19
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/handoff.d.ts +16 -1
- package/dist/handoff.js +91 -16
- package/dist/index.d.ts +10 -7
- package/dist/index.js +15 -7
- package/dist/message-parts.d.ts +11 -9
- package/dist/message-parts.js +11 -9
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +9 -2
- package/dist/verifier.js +28 -12
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +419 -0
- package/src/case.ts +651 -156
- package/src/cli.ts +1325 -100
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +142 -21
- package/src/exit-codes.ts +54 -0
- package/src/grading.ts +109 -52
- package/src/handoff.ts +108 -17
- package/src/index.ts +60 -3
- package/src/message-parts.ts +11 -9
- package/src/types.ts +5 -0
- package/src/verifier.ts +35 -13
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +56 -3
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +1102 -0
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/dist/case.js
CHANGED
|
@@ -166,7 +166,9 @@
|
|
|
166
166
|
* gets, since that reads wrapper keys.
|
|
167
167
|
*
|
|
168
168
|
* Not exits: a record whose envelope verifies and whose content does not
|
|
169
|
-
* match its commitment is `RECORD_CONTENT_MISMATCH
|
|
169
|
+
* match its commitment is `RECORD_CONTENT_MISMATCH` where that content is
|
|
170
|
+
* inside the signed body and `RECORD_CONTENT_DEPENDENT` where it was handed
|
|
171
|
+
* in beside the pack (proof#159), and one that fails
|
|
170
172
|
* another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
|
|
171
173
|
* Both are named and both STAY in the pile, so neither unsigned content
|
|
172
174
|
* handed in beside a pack nor a broken inclusion proof removes a record
|
|
@@ -185,7 +187,8 @@
|
|
|
185
187
|
* (F2).
|
|
186
188
|
* 10. **`counterpart_absent`** (`continue`). Skips the ambiguity and pair
|
|
187
189
|
* reporting, and there is nothing to skip: 4.1 emits that finding only
|
|
188
|
-
*
|
|
190
|
+
* where no candidate is left to read, an EMPTY candidate list or one whose
|
|
191
|
+
* every candidate another record accounts for (4.1 v1.17, A20). Which of the three counterpart kinds it
|
|
189
192
|
* gets reads the keys of the packs that verified, counted before exit 4,
|
|
190
193
|
* and whether any pack took exit 2 or 3.
|
|
191
194
|
*
|
|
@@ -206,7 +209,9 @@
|
|
|
206
209
|
* verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
|
|
207
210
|
* findings beside a verdict in 4.1.
|
|
208
211
|
*
|
|
209
|
-
* A case with
|
|
212
|
+
* A case with any `failed` contribution is `failed`: a proven failure keeps
|
|
213
|
+
* the headline (5.5.4 v1.16, A17, proof#158). Otherwise a case with more than
|
|
214
|
+
* one outcome is `mixed` (anchor#198, criterion 4).
|
|
210
215
|
*
|
|
211
216
|
* WHAT THIS READER DOES NOT CLAIM
|
|
212
217
|
* ===============================
|
|
@@ -225,20 +230,21 @@
|
|
|
225
230
|
* with that verdict and its reasons, so the case never reads above its
|
|
226
231
|
* weakest pack. On-chain re-fetch and `gradePack` are a caller's to run.
|
|
227
232
|
*
|
|
228
|
-
* THE COST OF 4.1's "GRADE EVERY CANDIDATE",
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
233
|
+
* THE COST OF 4.1's "GRADE EVERY CANDIDATE", AND WHAT IS LEFT OF IT (4.1
|
|
234
|
+
* v1.17, amendment A20, which reopens proof#134). Up to v1.16 a pile holding
|
|
235
|
+
* two or more records of the complementary type signed by one counterparty key
|
|
236
|
+
* could not reach `verified`. 4.1 now sets such a candidate aside where a
|
|
237
|
+
* record the subject's own key signed pairs with it verified at another
|
|
238
|
+
* sequence, so two messages in one direction, and the demo's A to B, B to A,
|
|
239
|
+
* A to B, read clean. What is left surfaces as `COUNTERPART_AMBIGUOUS` beside
|
|
240
|
+
* the failure: fan-in, fan-out, and the same pair in two cases at the same
|
|
241
|
+
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
242
|
+
* that would reopen proof#137's door.
|
|
238
243
|
*/
|
|
239
|
-
import { checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
244
|
+
import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
240
245
|
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
241
246
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
247
|
+
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
242
248
|
// ---- The vocabulary ---------------------------------------------------------
|
|
243
249
|
/**
|
|
244
250
|
* The verdicts a case can reach. Every one is already in use in
|
|
@@ -309,7 +315,9 @@ export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"];
|
|
|
309
315
|
/**
|
|
310
316
|
* A conformant hand-off record with no counterpart among the records this
|
|
311
317
|
* reader read, where every pack in the case verified under a key this reader
|
|
312
|
-
* can read and NO pack in the case is signed by the key the record names
|
|
318
|
+
* can read and NO pack in the case is signed by the key the record names,
|
|
319
|
+
* or where every candidate it had was accounted for by another record under
|
|
320
|
+
* A20 (the exception then carries `count`, and names no pack).
|
|
313
321
|
* **An exception, never a failure** (anchor#198, criterion 3).
|
|
314
322
|
*
|
|
315
323
|
* That is ALL it says. It does not say the counterparty's account is absent,
|
|
@@ -395,9 +403,11 @@ export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS = "counterpart_absent_
|
|
|
395
403
|
*/
|
|
396
404
|
export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
|
|
397
405
|
/**
|
|
398
|
-
* More than one candidate counterpart (4.1
|
|
399
|
-
* reading is the worst of them and the ambiguity is named: a reader
|
|
400
|
-
* to take one match can be handed an honest decoy beside a tampered
|
|
406
|
+
* More than one candidate counterpart that nothing accounts for (4.1 v1.17,
|
|
407
|
+
* A20). The reading is the worst of them and the ambiguity is named: a reader
|
|
408
|
+
* permitted to take one match can be handed an honest decoy beside a tampered
|
|
409
|
+
* record. `count` is how many of the candidates are live, which is the number
|
|
410
|
+
* the ambiguity is about.
|
|
401
411
|
*/
|
|
402
412
|
export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
|
|
403
413
|
/**
|
|
@@ -573,17 +583,26 @@ export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
|
573
583
|
*/
|
|
574
584
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
575
585
|
/**
|
|
576
|
-
* The pack's wrapper HOLDS and its
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
586
|
+
* The pack's wrapper HOLDS and its CASE GRADE is not `verified`
|
|
587
|
+
* (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
|
|
588
|
+
* `pack_bundle_not_verified`, which carried the bundle status alone). The case
|
|
589
|
+
* grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
|
|
590
|
+
* with the side map empty, and the FULL grade over the run with it. The signed
|
|
591
|
+
* grade decides where it is `failed` or `malformed`; a failure only the full
|
|
592
|
+
* grade reaches is `incomplete`, because it rests on bytes nobody signed;
|
|
593
|
+
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
594
|
+
* a body that is not one of the two event-carrying bodies takes the signed
|
|
595
|
+
* run's status, and a verifier that graded a different number of records than
|
|
596
|
+
* the pack holds takes the signed status, or `malformed` where that status is
|
|
597
|
+
* `verified`.
|
|
598
|
+
*
|
|
599
|
+
* The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
|
|
600
|
+
* kinds of the grade that decided it (none in the first three branches); and
|
|
601
|
+
* `reasons`, the verifier's reasons from the run with the side map empty. It
|
|
602
|
+
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
603
|
+
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
585
604
|
*/
|
|
586
|
-
export const
|
|
605
|
+
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
587
606
|
/**
|
|
588
607
|
* A record in a pack whose wrapper holds, whose OWN envelope does not verify
|
|
589
608
|
* (`EventResult.envelopeOk` is false). Not read by 4.1, and NAMED with the
|
|
@@ -604,12 +623,39 @@ export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify"
|
|
|
604
623
|
export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
|
|
605
624
|
/**
|
|
606
625
|
* A record whose envelope verifies and whose disclosed content does not hash to
|
|
607
|
-
* the
|
|
608
|
-
*
|
|
626
|
+
* the hash it commits to, its `content_hash` or a hand-off record's
|
|
627
|
+
* `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
|
|
628
|
+
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
629
|
+
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
630
|
+
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
609
631
|
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
610
632
|
* party erasing an honest verified pair by swapping it.
|
|
611
633
|
*/
|
|
612
634
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
635
|
+
/**
|
|
636
|
+
* The two readings proof#147 did not reach, given its shape (proof#159, ruled
|
|
637
|
+
* by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
|
|
638
|
+
* bundle verdict is `failed` ONLY when read with the unsigned content handed
|
|
639
|
+
* in beside it, and `failed` no longer when read with that map empty, is
|
|
640
|
+
* `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
|
|
641
|
+
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
642
|
+
* reader established that the bytes it was handed are not the bytes the record
|
|
643
|
+
* commits to, and nothing about the party that signed the record, because
|
|
644
|
+
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
645
|
+
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
646
|
+
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
647
|
+
* have become the case's headline.
|
|
648
|
+
*
|
|
649
|
+
* The test that separates the two is the one proof#159 states for the inverse:
|
|
650
|
+
* every deviation a signature covers still reads `failed` with the side map
|
|
651
|
+
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
652
|
+
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
653
|
+
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
654
|
+
* each still `failed`. Emptying the map is the most a party that signed
|
|
655
|
+
* nothing can do to starve the check, and it cannot remove any of those.
|
|
656
|
+
*/
|
|
657
|
+
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
658
|
+
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
613
659
|
/**
|
|
614
660
|
* A record signed under a ratified profile that is under quarantine
|
|
615
661
|
* (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
|
|
@@ -619,11 +665,13 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
619
665
|
/** The reader was handed no packs at all. */
|
|
620
666
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
621
667
|
/**
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
668
|
+
* The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
|
|
669
|
+
* printed in the scope sentence so a clean verdict never hides either.
|
|
670
|
+
* `pack_undeclared` is a note only where the reading holds no hand-off record;
|
|
671
|
+
* where it holds one, it is the exception of that name.
|
|
625
672
|
*/
|
|
626
|
-
export const
|
|
673
|
+
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
674
|
+
export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION];
|
|
627
675
|
/**
|
|
628
676
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
629
677
|
* them and a test may assert over them; nothing outside this array is emitted.
|
|
@@ -647,13 +695,14 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
647
695
|
PACK_HOLDS_NO_RECORDS,
|
|
648
696
|
PACK_KEY_UNREADABLE,
|
|
649
697
|
PACK_DOES_NOT_VERIFY,
|
|
650
|
-
|
|
698
|
+
PACK_GRADE_NOT_VERIFIED,
|
|
699
|
+
PACK_BUNDLE_CONTENT_DEPENDENT,
|
|
651
700
|
RECORD_ENVELOPE_DOES_NOT_VERIFY,
|
|
652
701
|
RECORD_DOES_NOT_VERIFY_IN_ITS_PACK,
|
|
653
702
|
RECORD_CONTENT_MISMATCH,
|
|
703
|
+
RECORD_CONTENT_DEPENDENT,
|
|
654
704
|
RECORD_PROFILE_QUARANTINED,
|
|
655
705
|
CASE_HOLDS_NO_PACKS,
|
|
656
|
-
CASE_HOLDS_NO_HANDOFF_RECORDS,
|
|
657
706
|
];
|
|
658
707
|
/**
|
|
659
708
|
* What each exception kind contributes to the case verdict. It is a TABLE and
|
|
@@ -666,10 +715,10 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
666
715
|
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
667
716
|
* hold.
|
|
668
717
|
*
|
|
669
|
-
* `
|
|
670
|
-
* contributes the pack's
|
|
671
|
-
*
|
|
672
|
-
*
|
|
718
|
+
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
719
|
+
* contributes the pack's case grade, which is never `verified` (the exception
|
|
720
|
+
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
721
|
+
* it from `status`.
|
|
673
722
|
*/
|
|
674
723
|
const EXCEPTION_VERDICT = {
|
|
675
724
|
[COUNTERPART_ABSENT]: "incomplete",
|
|
@@ -690,14 +739,26 @@ const EXCEPTION_VERDICT = {
|
|
|
690
739
|
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
691
740
|
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
692
741
|
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
693
|
-
[
|
|
742
|
+
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
743
|
+
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
694
744
|
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
695
745
|
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
696
746
|
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
747
|
+
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
697
748
|
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
698
749
|
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
699
|
-
[CASE_HOLDS_NO_HANDOFF_RECORDS]: "partial",
|
|
700
750
|
};
|
|
751
|
+
/**
|
|
752
|
+
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
753
|
+
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
754
|
+
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
755
|
+
*/
|
|
756
|
+
const GRADE_CONTRIBUTES = new Set([
|
|
757
|
+
"partial",
|
|
758
|
+
"incomplete",
|
|
759
|
+
"failed",
|
|
760
|
+
"malformed",
|
|
761
|
+
]);
|
|
701
762
|
// ---- The reading ------------------------------------------------------------
|
|
702
763
|
const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
|
|
703
764
|
? bundle["body_type"]
|
|
@@ -735,6 +796,23 @@ const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
|
|
|
735
796
|
* bundle-level error at all, and an exception that named the verdict and no
|
|
736
797
|
* reason would send a reader looking for a defect in the pack as a whole.
|
|
737
798
|
*/
|
|
799
|
+
/**
|
|
800
|
+
* The reasons a reading carries only because of the side map, in the order the
|
|
801
|
+
* verifier wrote them: what the content-dependent kinds are about, and nothing
|
|
802
|
+
* a signature covers.
|
|
803
|
+
*/
|
|
804
|
+
const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
|
|
805
|
+
/**
|
|
806
|
+
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
807
|
+
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
808
|
+
*/
|
|
809
|
+
function sideMapHasKeys(content) {
|
|
810
|
+
if (content === null || content === undefined)
|
|
811
|
+
return false;
|
|
812
|
+
if (content instanceof Map)
|
|
813
|
+
return content.size > 0;
|
|
814
|
+
return Object.keys(content).length > 0;
|
|
815
|
+
}
|
|
738
816
|
const bundleReasons = (result) => [
|
|
739
817
|
...result.errors,
|
|
740
818
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -770,8 +848,13 @@ function underQuarantine(specVersion) {
|
|
|
770
848
|
/**
|
|
771
849
|
* The case verdict, from the contributions alone.
|
|
772
850
|
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
851
|
+
* **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
|
|
852
|
+
* A17, proof#158): if any contribution is `failed`, the case is `failed`,
|
|
853
|
+
* whatever else contributes, and every other exception stays named beside it.
|
|
854
|
+
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
855
|
+
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
856
|
+
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
857
|
+
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
775
858
|
*
|
|
776
859
|
* THE INVARIANT, stated so a mutation test has something to kill: `verified` is
|
|
777
860
|
* reachable ONLY when `exceptions` is empty. Every exception kind carries a
|
|
@@ -780,14 +863,25 @@ function underQuarantine(specVersion) {
|
|
|
780
863
|
* The assertion is here anyway, because that is the property every one of the
|
|
781
864
|
* seventeen findings on this program violated.
|
|
782
865
|
*/
|
|
783
|
-
function verdictOf(exceptions, contributions) {
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
866
|
+
function verdictOf(exceptions, contributions, holdsPacks = true) {
|
|
867
|
+
// A reading that holds at least one pack and no contribution at all is
|
|
868
|
+
// `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
|
|
869
|
+
// nothing in it was an exception. Every exception contributes, so no
|
|
870
|
+
// contribution means no exception. A reading with no packs always carries
|
|
871
|
+
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
872
|
+
// nothing reading as `verified` is the defect class itself.
|
|
873
|
+
if (contributions.length === 0) {
|
|
874
|
+
if (exceptions.length > 0) {
|
|
875
|
+
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
876
|
+
}
|
|
877
|
+
return holdsPacks ? "verified" : "partial";
|
|
878
|
+
}
|
|
789
879
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
790
|
-
const verdict = outcomes.
|
|
880
|
+
const verdict = outcomes.has("failed")
|
|
881
|
+
? "failed"
|
|
882
|
+
: outcomes.size === 1
|
|
883
|
+
? [...outcomes][0]
|
|
884
|
+
: "mixed";
|
|
791
885
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
792
886
|
throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
793
887
|
}
|
|
@@ -821,23 +915,35 @@ function verdictOf(exceptions, contributions) {
|
|
|
821
915
|
* see is the counterpart that is missing.
|
|
822
916
|
*/
|
|
823
917
|
export async function readCase(packs) {
|
|
918
|
+
// A `null` slot is held for each pack that carries no declaration, and
|
|
919
|
+
// settled once the pile says whether the reading holds a hand-off record.
|
|
824
920
|
const exceptions = [];
|
|
825
921
|
const contributions = [];
|
|
826
922
|
const note = (kind, fields = {}) => {
|
|
827
923
|
exceptions.push({ kind, ...fields });
|
|
828
|
-
// `null` is
|
|
829
|
-
|
|
924
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
925
|
+
// verified, and `incomplete` for a word a pack grade passes through and a
|
|
926
|
+
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
927
|
+
let contribution = EXCEPTION_VERDICT[kind];
|
|
928
|
+
if (contribution === null) {
|
|
929
|
+
const status = fields.status;
|
|
930
|
+
contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
|
|
931
|
+
}
|
|
830
932
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
831
933
|
};
|
|
832
934
|
if (packs.length === 0) {
|
|
833
935
|
note(CASE_HOLDS_NO_PACKS);
|
|
936
|
+
const named = exceptions;
|
|
937
|
+
const contributed = contributions;
|
|
834
938
|
return {
|
|
835
|
-
verdict: verdictOf(
|
|
836
|
-
exceptions,
|
|
939
|
+
verdict: verdictOf(named, contributed, false),
|
|
940
|
+
exceptions: named,
|
|
837
941
|
records: [],
|
|
838
|
-
contributions,
|
|
942
|
+
contributions: contributed,
|
|
839
943
|
handoffRecords: 0,
|
|
840
944
|
onChainChecked: false,
|
|
945
|
+
notes: [],
|
|
946
|
+
packs: [],
|
|
841
947
|
};
|
|
842
948
|
}
|
|
843
949
|
// Step 1 and 2, pack by pack, in the order the reader was given them.
|
|
@@ -879,6 +985,15 @@ export async function readCase(packs) {
|
|
|
879
985
|
// below, from the pack envelope and never from the body, so no body type, no
|
|
880
986
|
// disclosure choice and no record content decides whether it is read.
|
|
881
987
|
const holderKeys = new Set();
|
|
988
|
+
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
989
|
+
const packReadings = [];
|
|
990
|
+
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
991
|
+
const caseNotes = [];
|
|
992
|
+
// The role each pack's readable v2 declaration signs for each record, keyed
|
|
993
|
+
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
994
|
+
const rolesByPack = new Map();
|
|
995
|
+
// The undeclared packs' slots, settled after the pile is read.
|
|
996
|
+
const undeclaredSlots = [];
|
|
882
997
|
// How many packs this reader could not tie to a key: an unreadable wrapper
|
|
883
998
|
// key, or a wrapper that does not hold. While it is not zero, "no pack in this
|
|
884
999
|
// case is signed by the named key" is not established, so the innocent
|
|
@@ -894,6 +1009,13 @@ export async function readCase(packs) {
|
|
|
894
1009
|
// of these spellings (proof#135) and the reading must not.
|
|
895
1010
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
896
1011
|
unattributed += 1;
|
|
1012
|
+
packReadings.push({
|
|
1013
|
+
ref: pack.ref,
|
|
1014
|
+
signingKey: null,
|
|
1015
|
+
grade: null,
|
|
1016
|
+
recordsWithheld: 0,
|
|
1017
|
+
declarationVersion: null,
|
|
1018
|
+
});
|
|
897
1019
|
continue;
|
|
898
1020
|
}
|
|
899
1021
|
// The precondition `HeldRecord` states, met HERE rather than trusted to a
|
|
@@ -905,28 +1027,121 @@ export async function readCase(packs) {
|
|
|
905
1027
|
content: (pack.content ?? undefined),
|
|
906
1028
|
});
|
|
907
1029
|
witnessChecked.push(verified.onChainChecked);
|
|
1030
|
+
// The same pack with the side map EMPTY (proof#159): what fails here is
|
|
1031
|
+
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1032
|
+
// signed. Skipped where there is no map, because the two runs are then the
|
|
1033
|
+
// same run.
|
|
1034
|
+
const signed = sideMapHasKeys(pack.content)
|
|
1035
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1036
|
+
: verified;
|
|
908
1037
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
909
1038
|
if (!wrapperHolds(verified)) {
|
|
910
1039
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
911
1040
|
unattributed += 1;
|
|
1041
|
+
packReadings.push({
|
|
1042
|
+
ref: pack.ref,
|
|
1043
|
+
signingKey: holder,
|
|
1044
|
+
grade: null,
|
|
1045
|
+
recordsWithheld: 0,
|
|
1046
|
+
declarationVersion: null,
|
|
1047
|
+
});
|
|
912
1048
|
continue;
|
|
913
1049
|
}
|
|
914
1050
|
holderKeys.add(holder);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1051
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
1052
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
1053
|
+
// The per-record gradings below are joined to `records` BY INDEX, so a
|
|
1054
|
+
// verifier that graded a different number of records than the pack holds
|
|
1055
|
+
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1056
|
+
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1057
|
+
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1058
|
+
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1059
|
+
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1060
|
+
// than indexed into, and the pack's records are dropped exactly as a
|
|
1061
|
+
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1062
|
+
const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1063
|
+
const knownBody = CASE_BODY_TYPES.includes(bodyType);
|
|
1064
|
+
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1065
|
+
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1066
|
+
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1067
|
+
let check = null;
|
|
1068
|
+
let full = null;
|
|
1069
|
+
let causes = [];
|
|
1070
|
+
let grade;
|
|
1071
|
+
if (signed.status === "malformed") {
|
|
1072
|
+
grade = "malformed";
|
|
1073
|
+
}
|
|
1074
|
+
else if (!knownBody) {
|
|
1075
|
+
grade = signed.status;
|
|
1076
|
+
}
|
|
1077
|
+
else if (ungraded) {
|
|
1078
|
+
grade = signed.status === "verified" ? "malformed" : signed.status;
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
check = checkDeclaration(pack.bundle, pack.content);
|
|
1082
|
+
const completeness = completenessNotes(pack.bundle);
|
|
1083
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1084
|
+
const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null));
|
|
1085
|
+
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1086
|
+
// where it is `failed` or `malformed`; a failure only the side map
|
|
1087
|
+
// produced is `incomplete`; otherwise the full grade.
|
|
1088
|
+
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1089
|
+
grade = bareGrade.verdict;
|
|
1090
|
+
causes = bareGrade.causes.map((c) => c.kind);
|
|
1091
|
+
}
|
|
1092
|
+
else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1093
|
+
grade = "incomplete";
|
|
1094
|
+
causes = full.causes.map((c) => c.kind);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
grade = full.verdict;
|
|
1098
|
+
causes = full.causes.map((c) => c.kind);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (grade !== "verified") {
|
|
1102
|
+
const reasons = bundleReasons(signed);
|
|
1103
|
+
if (knownBody && ungraded && signed.status !== "malformed") {
|
|
1104
|
+
// Said, because nothing else says it: the verifier's own reasons are
|
|
1105
|
+
// about the bundle, and this pack's records are dropped for a reason
|
|
1106
|
+
// of this reader's.
|
|
1107
|
+
reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1108
|
+
"record(s) the pack holds, so none of them is read");
|
|
1109
|
+
}
|
|
1110
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
919
1111
|
packRef: pack.ref,
|
|
920
|
-
status:
|
|
921
|
-
|
|
1112
|
+
status: grade,
|
|
1113
|
+
causes,
|
|
1114
|
+
reasons,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1118
|
+
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1119
|
+
packRef: pack.ref,
|
|
1120
|
+
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1124
|
+
packReadings.push({
|
|
1125
|
+
ref: pack.ref,
|
|
1126
|
+
signingKey: holder,
|
|
1127
|
+
grade,
|
|
1128
|
+
recordsWithheld,
|
|
1129
|
+
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1130
|
+
});
|
|
1131
|
+
if (recordsWithheld > 0) {
|
|
1132
|
+
// A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
|
|
1133
|
+
// these records' content does not travel, and the scope sentence counts
|
|
1134
|
+
// them, so a clean verdict never hides them.
|
|
1135
|
+
caseNotes.push({
|
|
1136
|
+
position,
|
|
1137
|
+
note: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
|
|
922
1138
|
});
|
|
923
1139
|
}
|
|
924
1140
|
// The body is one the verifier could not read, so it graded no record, and
|
|
925
1141
|
// nothing below has a grading to read. Named above.
|
|
926
|
-
if (
|
|
1142
|
+
if (signed.status === "malformed")
|
|
927
1143
|
continue;
|
|
928
|
-
|
|
929
|
-
if (!CASE_BODY_TYPES.includes(bodyType)) {
|
|
1144
|
+
if (!knownBody) {
|
|
930
1145
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
931
1146
|
// records sit under `body.source_bundle`, and reading them is a change to
|
|
932
1147
|
// which packs this reader grades, so it is a decision of its own and not
|
|
@@ -936,58 +1151,39 @@ export async function readCase(packs) {
|
|
|
936
1151
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
937
1152
|
continue;
|
|
938
1153
|
}
|
|
939
|
-
|
|
940
|
-
//
|
|
941
|
-
|
|
942
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
943
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
944
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
945
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
946
|
-
// must never crash on a pack a party can build, so the gap is NAMED rather
|
|
947
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
948
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3: under the
|
|
949
|
-
// mutation that dropped `bodyHashOk` from the wrapper test, this kernel
|
|
950
|
-
// threw `Cannot read properties of undefined (reading 'envelopeOk')` here
|
|
951
|
-
// and nothing in the ladder said so).
|
|
952
|
-
if (verified.events.length !== records.length) {
|
|
953
|
-
note(PACK_BUNDLE_NOT_VERIFIED, {
|
|
954
|
-
packRef: pack.ref,
|
|
955
|
-
status: verified.status,
|
|
956
|
-
reasons: [
|
|
957
|
-
`this verifier graded ${verified.events.length} of the ` +
|
|
958
|
-
`${records.length} record(s) the pack holds, so none of them is read`,
|
|
959
|
-
],
|
|
960
|
-
});
|
|
1154
|
+
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1155
|
+
// none of its records are read.
|
|
1156
|
+
if (ungraded)
|
|
961
1157
|
continue;
|
|
962
|
-
}
|
|
963
1158
|
if (records.length === 0) {
|
|
964
1159
|
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
965
1160
|
continue;
|
|
966
1161
|
}
|
|
967
|
-
const
|
|
1162
|
+
const checked = check;
|
|
968
1163
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
969
|
-
|
|
1164
|
+
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1165
|
+
if (checked.contentFindings.length > 0) {
|
|
970
1166
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
971
1167
|
// both, over different records, and the two say different things. This
|
|
972
1168
|
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
973
1169
|
// it turns on (proof#147).
|
|
974
1170
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
975
1171
|
packRef: pack.ref,
|
|
976
|
-
count:
|
|
1172
|
+
count: checked.contentFindings.length,
|
|
977
1173
|
});
|
|
978
1174
|
}
|
|
979
|
-
if (
|
|
1175
|
+
if (checked.verdict === "failed") {
|
|
980
1176
|
note(PACK_DECLARATION_DEVIATES, {
|
|
981
1177
|
packRef: pack.ref,
|
|
982
|
-
count:
|
|
1178
|
+
count: checked.deviations.length,
|
|
983
1179
|
});
|
|
984
1180
|
}
|
|
985
|
-
else if (
|
|
1181
|
+
else if (checked.contentFindings.length > 0) {
|
|
986
1182
|
// Already named above. `partial` here is not "the reader cannot read the
|
|
987
1183
|
// claim": the claim was read, and part of it rests on bytes nobody
|
|
988
1184
|
// signed.
|
|
989
1185
|
}
|
|
990
|
-
else if (
|
|
1186
|
+
else if (checked.verdict === "verified") {
|
|
991
1187
|
// A pack that agrees with its own claim contributes NOTHING, rather than
|
|
992
1188
|
// a `verified` of its own. The case verdict is over the hand-off
|
|
993
1189
|
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
@@ -996,7 +1192,7 @@ export async function readCase(packs) {
|
|
|
996
1192
|
// read `mixed` and the failure would be the quieter half of a two-word
|
|
997
1193
|
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
998
1194
|
}
|
|
999
|
-
else if (
|
|
1195
|
+
else if (checked.present) {
|
|
1000
1196
|
// `partial` with a declaration present is 5.4.3's "a claim this reader
|
|
1001
1197
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1002
1198
|
// same posture here, for the same reason: the field has no accept-set and
|
|
@@ -1004,7 +1200,18 @@ export async function readCase(packs) {
|
|
|
1004
1200
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1005
1201
|
}
|
|
1006
1202
|
else {
|
|
1007
|
-
|
|
1203
|
+
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
1204
|
+
// A23, I4): `partial` where the reading holds a hand-off record, and a
|
|
1205
|
+
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1206
|
+
// the slot is held HERE, in pack order, and settled below.
|
|
1207
|
+
undeclaredSlots.push({
|
|
1208
|
+
exceptionAt: exceptions.length,
|
|
1209
|
+
contributionAt: contributions.length,
|
|
1210
|
+
position,
|
|
1211
|
+
ref: pack.ref,
|
|
1212
|
+
});
|
|
1213
|
+
exceptions.push(null);
|
|
1214
|
+
contributions.push(null);
|
|
1008
1215
|
}
|
|
1009
1216
|
// Each record on its OWN envelope. `packRecords` and the verifier both walk
|
|
1010
1217
|
// `body.events` in order and emit one entry per element, so the index is
|
|
@@ -1012,6 +1219,7 @@ export async function readCase(packs) {
|
|
|
1012
1219
|
for (let index = 0; index < records.length; index += 1) {
|
|
1013
1220
|
const record = records[index];
|
|
1014
1221
|
const graded = verified.events[index];
|
|
1222
|
+
const bare = signed.events[index];
|
|
1015
1223
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1016
1224
|
note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
|
|
1017
1225
|
continue;
|
|
@@ -1038,20 +1246,31 @@ export async function readCase(packs) {
|
|
|
1038
1246
|
});
|
|
1039
1247
|
continue;
|
|
1040
1248
|
}
|
|
1041
|
-
// Named, and the record stays in the pile (see the kinds).
|
|
1042
|
-
|
|
1249
|
+
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
1250
|
+
// the signed body carries on its own is `failed`; one that appears only
|
|
1251
|
+
// with the side map is `incomplete` (proof#159).
|
|
1252
|
+
if (bare.contentOk === false) {
|
|
1043
1253
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1044
1254
|
packRef: pack.ref,
|
|
1045
1255
|
eventId: record.eventId,
|
|
1046
|
-
reasons: [...
|
|
1256
|
+
reasons: [...bare.errors],
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
else if (graded.contentOk === false) {
|
|
1260
|
+
note(RECORD_CONTENT_DEPENDENT, {
|
|
1261
|
+
packRef: pack.ref,
|
|
1262
|
+
eventId: record.eventId,
|
|
1263
|
+
reasons: onlyIn(graded.errors, bare.errors),
|
|
1047
1264
|
});
|
|
1048
1265
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1266
|
+
// Every other check on the SIGNED reading, so side content can neither
|
|
1267
|
+
// add a failure here nor take one away.
|
|
1268
|
+
if (!(bare.inclusionOk && bare.checkpointBound) ||
|
|
1269
|
+
(!bare.ok && bare.contentOk !== false)) {
|
|
1051
1270
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1052
1271
|
packRef: pack.ref,
|
|
1053
1272
|
eventId: record.eventId,
|
|
1054
|
-
reasons: [...
|
|
1273
|
+
reasons: [...bare.errors],
|
|
1055
1274
|
});
|
|
1056
1275
|
}
|
|
1057
1276
|
held.push({
|
|
@@ -1080,6 +1299,7 @@ export async function readCase(packs) {
|
|
|
1080
1299
|
findings: [...reading.findings],
|
|
1081
1300
|
failedChecks: [...reading.failedChecks],
|
|
1082
1301
|
counterparts: [...reading.candidates],
|
|
1302
|
+
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1083
1303
|
});
|
|
1084
1304
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1085
1305
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
@@ -1093,9 +1313,13 @@ export async function readCase(packs) {
|
|
|
1093
1313
|
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1094
1314
|
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1095
1315
|
// party switched off by breaking its own record.
|
|
1316
|
+
//
|
|
1317
|
+
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1318
|
+
// the declaration check already ran on the signed body.
|
|
1096
1319
|
const claims = claimsByPack.get(position);
|
|
1097
1320
|
if (claims !== undefined &&
|
|
1098
1321
|
claims.readable &&
|
|
1322
|
+
claims.version === DECLARATION_VERSION &&
|
|
1099
1323
|
!claims.claimedSides.includes(eventId)) {
|
|
1100
1324
|
note(RECORD_UNCLAIMED, { packRef, eventId });
|
|
1101
1325
|
}
|
|
@@ -1135,19 +1359,36 @@ export async function readCase(packs) {
|
|
|
1135
1359
|
// that party's pack. Where some pack could not be tied to a key, "no
|
|
1136
1360
|
// pack is signed by the named key" is not established and the reading
|
|
1137
1361
|
// says it could not tell.
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1362
|
+
//
|
|
1363
|
+
// EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
|
|
1364
|
+
// absence is a finding about the candidates the reader DID read, each
|
|
1365
|
+
// paired `verified` with another record the subject's own signer made,
|
|
1366
|
+
// and not a finding about any pack. 4.1 and evidence-bundle A16 name the
|
|
1367
|
+
// neutral kind for it, and the damning one would put the audited party's
|
|
1368
|
+
// own unfaithful record on the counterparty's pack as a statement
|
|
1369
|
+
// against it. `count` is how many candidates were set aside, which is
|
|
1370
|
+
// also what tells the rendered sentence which absence this is.
|
|
1371
|
+
const graded = reading.candidateReadings;
|
|
1372
|
+
const setAside = graded.length > 0 && graded.every((c) => c.accountedFor !== null)
|
|
1373
|
+
? graded.length
|
|
1374
|
+
: 0;
|
|
1375
|
+
const kind = setAside > 0
|
|
1376
|
+
? COUNTERPART_ABSENT
|
|
1377
|
+
: named !== null && holderKeys.has(named)
|
|
1378
|
+
? COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS
|
|
1379
|
+
: unattributed > 0
|
|
1380
|
+
? COUNTERPART_ABSENT_SIGNER_UNKNOWN
|
|
1381
|
+
: COUNTERPART_ABSENT;
|
|
1382
|
+
note(kind, setAside > 0
|
|
1383
|
+
? { packRef, eventId, counterpartyKey: named, count: setAside }
|
|
1384
|
+
: { packRef, eventId, counterpartyKey: named });
|
|
1144
1385
|
continue;
|
|
1145
1386
|
}
|
|
1146
1387
|
if (reading.findings.includes(COUNTERPART_AMBIGUOUS)) {
|
|
1147
1388
|
note(COUNTERPART_AMBIGUOUS, {
|
|
1148
1389
|
packRef,
|
|
1149
1390
|
eventId,
|
|
1150
|
-
count: reading.
|
|
1391
|
+
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1151
1392
|
});
|
|
1152
1393
|
}
|
|
1153
1394
|
// ON `failedChecks` AND NOT ON THE VERDICT WORD. 4.1 keeps the UNION of
|
|
@@ -1168,17 +1409,54 @@ export async function readCase(packs) {
|
|
|
1168
1409
|
contributions.push([`${packRef}/${eventId}`, "verified"]);
|
|
1169
1410
|
}
|
|
1170
1411
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1412
|
+
// A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
|
|
1413
|
+
// A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
|
|
1414
|
+
// contributing `partial`, in the slot it was given in pack order where the
|
|
1415
|
+
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1416
|
+
// none.
|
|
1417
|
+
for (const slot of undeclaredSlots) {
|
|
1418
|
+
if (handoffRecords > 0) {
|
|
1419
|
+
exceptions[slot.exceptionAt] = { kind: PACK_UNDECLARED, packRef: slot.ref };
|
|
1420
|
+
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED]];
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
caseNotes.push({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const named = exceptions.filter((e) => e !== null);
|
|
1427
|
+
const contributed = contributions.filter((c) => c !== null);
|
|
1428
|
+
// Pack order, stable. A pack carries at most one note: one with no
|
|
1429
|
+
// declaration withholds nothing by one.
|
|
1430
|
+
const notes = caseNotes
|
|
1431
|
+
.map((n, i) => ({ ...n, i }))
|
|
1432
|
+
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1433
|
+
.map((n) => n.note);
|
|
1173
1434
|
return {
|
|
1174
|
-
verdict: verdictOf(
|
|
1175
|
-
exceptions,
|
|
1435
|
+
verdict: verdictOf(named, contributed),
|
|
1436
|
+
exceptions: named,
|
|
1176
1437
|
records,
|
|
1177
|
-
contributions,
|
|
1438
|
+
contributions: contributed,
|
|
1178
1439
|
handoffRecords,
|
|
1179
1440
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1441
|
+
notes,
|
|
1442
|
+
packs: packReadings,
|
|
1180
1443
|
};
|
|
1181
1444
|
}
|
|
1445
|
+
/**
|
|
1446
|
+
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1447
|
+
* and `null` otherwise.
|
|
1448
|
+
*/
|
|
1449
|
+
function declarationVersionOf(bundle) {
|
|
1450
|
+
const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
1451
|
+
? v[k]
|
|
1452
|
+
: undefined;
|
|
1453
|
+
const declaration = at(at(bundle, "body"), "declaration");
|
|
1454
|
+
if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
const version = declaration["declaration_version"];
|
|
1458
|
+
return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
|
|
1459
|
+
}
|
|
1182
1460
|
// ---- Presentation (optional) ------------------------------------------------
|
|
1183
1461
|
/**
|
|
1184
1462
|
* The house wording for one exception.
|
|
@@ -1192,6 +1470,14 @@ export function renderException(exception) {
|
|
|
1192
1470
|
const where = `${exception.packRef}/${exception.eventId}`;
|
|
1193
1471
|
switch (exception.kind) {
|
|
1194
1472
|
case COUNTERPART_ABSENT:
|
|
1473
|
+
if (exception.count) {
|
|
1474
|
+
const which = exception.count === 1
|
|
1475
|
+
? "its one candidate counterpart pairs"
|
|
1476
|
+
: `each of its ${exception.count} candidate counterparts pairs`;
|
|
1477
|
+
return (`${where} has no counterpart among the records this verifier read: ` +
|
|
1478
|
+
`${which} verified with another record signed by the key that ` +
|
|
1479
|
+
"signed this one, at a different sequence, and is set aside");
|
|
1480
|
+
}
|
|
1195
1481
|
return (`${where} has no counterpart among the records this verifier read, ` +
|
|
1196
1482
|
"and no pack in this case is signed by the key it names");
|
|
1197
1483
|
case COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS:
|
|
@@ -1255,13 +1541,20 @@ export function renderException(exception) {
|
|
|
1255
1541
|
return (`${exception.packRef} carries a wrapper signature over its body that ` +
|
|
1256
1542
|
"does not verify, so none of its records are read and the key it " +
|
|
1257
1543
|
"names is not counted as its signer");
|
|
1258
|
-
case
|
|
1259
|
-
let head = `${exception.packRef}
|
|
1260
|
-
|
|
1544
|
+
case PACK_GRADE_NOT_VERIFIED: {
|
|
1545
|
+
let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
|
|
1546
|
+
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
1547
|
+
head += ` (${exception.causes.join(", ")})`;
|
|
1548
|
+
}
|
|
1261
1549
|
if (exception.status === "malformed")
|
|
1262
1550
|
head += ", so none of its records are read";
|
|
1263
1551
|
return head + because(exception.reasons);
|
|
1264
1552
|
}
|
|
1553
|
+
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
1554
|
+
return (`${exception.packRef} reads FAILED as a bundle only together with ` +
|
|
1555
|
+
"the unsigned content handed in beside it, which anyone could have " +
|
|
1556
|
+
"supplied, so whether that failure is the pack's is UNKNOWN" +
|
|
1557
|
+
because(exception.reasons));
|
|
1265
1558
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
1266
1559
|
return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
|
|
1267
1560
|
"does not verify, so it is not read as a hand-off record" +
|
|
@@ -1274,45 +1567,128 @@ export function renderException(exception) {
|
|
|
1274
1567
|
return (`${where} is read with content that does not match what it commits ` +
|
|
1275
1568
|
"to, and it is still read as a hand-off record" +
|
|
1276
1569
|
because(exception.reasons));
|
|
1570
|
+
case RECORD_CONTENT_DEPENDENT:
|
|
1571
|
+
return (`${where} commits to content that the unsigned content handed in ` +
|
|
1572
|
+
"beside its pack does not match, and anyone could have supplied " +
|
|
1573
|
+
"that content, so what it says about the record is UNKNOWN; the " +
|
|
1574
|
+
"record is still read as a hand-off record" +
|
|
1575
|
+
because(exception.reasons));
|
|
1277
1576
|
case RECORD_PROFILE_QUARANTINED:
|
|
1278
1577
|
return (`${where} is signed under a profile this case reader does not read ` +
|
|
1279
|
-
"while its admission is undecided
|
|
1578
|
+
"while its admission is undecided, so what it says is UNKNOWN");
|
|
1280
1579
|
case CASE_HOLDS_NO_PACKS:
|
|
1281
1580
|
return "this case holds no packs";
|
|
1282
|
-
case CASE_HOLDS_NO_HANDOFF_RECORDS:
|
|
1283
|
-
return ("no record this verifier read in this case is a hand-off record, " +
|
|
1284
|
-
"so no hand-off was verified");
|
|
1285
1581
|
default:
|
|
1286
1582
|
return exception.kind;
|
|
1287
1583
|
}
|
|
1288
1584
|
}
|
|
1585
|
+
/** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
|
|
1586
|
+
export function renderCaseNote(note) {
|
|
1587
|
+
switch (note.kind) {
|
|
1588
|
+
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
1589
|
+
return (`${note.packRef} withholds the content of ${note.count} record(s) by ` +
|
|
1590
|
+
"its own signed declaration: their signatures and commitments were " +
|
|
1591
|
+
"checked, their content was not");
|
|
1592
|
+
case PACK_UNDECLARED:
|
|
1593
|
+
return `${note.packRef} does not say what it holds`;
|
|
1594
|
+
default:
|
|
1595
|
+
return note.kind;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1289
1598
|
/**
|
|
1290
|
-
*
|
|
1599
|
+
* The scope sentence, printed directly under the headline (evidence-bundle
|
|
1600
|
+
* 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
|
|
1601
|
+
*
|
|
1602
|
+
* The packs in the reading; the distinct keys, compared as keys, that signed
|
|
1603
|
+
* the wrapper of a pack whose wrapper held; the hand-off records; the
|
|
1604
|
+
* exceptions; and that the reading was made offline. Where any pack holds
|
|
1605
|
+
* records withheld by its declaration, their total; where any pack is noted
|
|
1606
|
+
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
1607
|
+
* MEANS, and a surface MUST print them with it.
|
|
1291
1608
|
*
|
|
1292
|
-
*
|
|
1293
|
-
* the
|
|
1294
|
-
* that differ by a word.
|
|
1609
|
+
* What was not checked on the public record is no longer a clause here. It is
|
|
1610
|
+
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1295
1611
|
*/
|
|
1296
1612
|
export function renderCaseSummary(reading) {
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
? ""
|
|
1309
|
-
: "; no anchor_ref in it was re-fetched on chain, which this reader never does";
|
|
1310
|
-
if (reading.verdict === "verified") {
|
|
1311
|
-
return (`this case verifies across ${reading.records.length} record(s): ` +
|
|
1312
|
-
`${n} hand-off record(s), every one matched, no exceptions` +
|
|
1313
|
-
scope);
|
|
1613
|
+
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
1614
|
+
const withheld = reading.packs.reduce((n, p) => n + p.recordsWithheld, 0);
|
|
1615
|
+
const undeclared = reading.notes.filter((n) => n.kind === PACK_UNDECLARED).length;
|
|
1616
|
+
const lines = [
|
|
1617
|
+
`${reading.packs.length} pack(s), ${keys.size} signing key(s), ` +
|
|
1618
|
+
`${reading.handoffRecords} hand-off record(s), ` +
|
|
1619
|
+
`${reading.exceptions.length} exception(s), read offline.`,
|
|
1620
|
+
];
|
|
1621
|
+
if (withheld > 0) {
|
|
1622
|
+
lines.push(`${withheld} record(s)' content withheld by their holder: their ` +
|
|
1623
|
+
"signatures and commitments were checked, their content was not.");
|
|
1314
1624
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1625
|
+
if (undeclared > 0)
|
|
1626
|
+
lines.push(`${undeclared} pack(s) do not say what they hold.`);
|
|
1627
|
+
return lines.join(" ");
|
|
1628
|
+
}
|
|
1629
|
+
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
1630
|
+
/**
|
|
1631
|
+
* The one display title of each of the eight 3 words. A surface prints the
|
|
1632
|
+
* title, never the word, and never a title of its own (#173, A18). The wording
|
|
1633
|
+
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
1634
|
+
* in the spec.
|
|
1635
|
+
*/
|
|
1636
|
+
export const VERDICT_DISPLAY_TITLES = {
|
|
1637
|
+
verified: "Verified",
|
|
1638
|
+
partial: "Partial",
|
|
1639
|
+
pending: "Pending",
|
|
1640
|
+
incomplete: "Incomplete",
|
|
1641
|
+
failed: "Failed",
|
|
1642
|
+
malformed: "Not readable",
|
|
1643
|
+
not_found: "Not found",
|
|
1644
|
+
mixed: "Mixed",
|
|
1645
|
+
};
|
|
1646
|
+
/**
|
|
1647
|
+
* What a reader may rely on, for each of the six words a case reading reaches
|
|
1648
|
+
* (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
|
|
1649
|
+
* never head a case reading: they are witness states.
|
|
1650
|
+
*/
|
|
1651
|
+
export const CASE_RELIANCE = {
|
|
1652
|
+
verified: "Every pack in this drop is genuine and unaltered and agrees with its " +
|
|
1653
|
+
"own signed declaration. Every hand-off with both sides here is " +
|
|
1654
|
+
"intact. Every other record is one its holder signed as a step. " +
|
|
1655
|
+
"Nothing here could not be checked. This was read offline, on this " +
|
|
1656
|
+
"device. It does not say these packs are everything the parties " +
|
|
1657
|
+
"recorded.",
|
|
1658
|
+
partial: "Nothing here was proven wrong, and a check could not be completed: a " +
|
|
1659
|
+
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
1660
|
+
"what it holds. The exceptions say which.",
|
|
1661
|
+
incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
|
|
1662
|
+
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
1663
|
+
"handed over, or a claim could not be read. Each exception says what " +
|
|
1664
|
+
"is missing and, where a key is known, whose pack would supply it.",
|
|
1665
|
+
failed: "A check ran and did not pass. Do not rely on this drop until the " +
|
|
1666
|
+
"named failure is explained. The failure is named on the pack or " +
|
|
1667
|
+
"record it is about, and every other exception is listed beside it.",
|
|
1668
|
+
malformed: "Part of this drop could not be read as evidence, and nothing was " +
|
|
1669
|
+
"proven wrong. The exceptions name what could not be read.",
|
|
1670
|
+
mixed: "The readings here differ and none of them failed: some are verified " +
|
|
1671
|
+
"and some could not be checked. Read each pack's row and each " +
|
|
1672
|
+
"exception before relying on any of it.",
|
|
1673
|
+
};
|
|
1674
|
+
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
1675
|
+
export const WITNESS_STATES = {
|
|
1676
|
+
confirmed: "Confirmed on the public record",
|
|
1677
|
+
not_checked: "Not checked",
|
|
1678
|
+
differs: "The public record holds different bytes",
|
|
1679
|
+
};
|
|
1680
|
+
/** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
|
|
1681
|
+
export const WITNESS_NOT_LOOKED_UP = Object.freeze({
|
|
1682
|
+
state: "not_checked",
|
|
1683
|
+
reasons: ["no lookup was run"],
|
|
1684
|
+
services: [],
|
|
1685
|
+
});
|
|
1686
|
+
/** The witness line as both kernels print it. */
|
|
1687
|
+
export function renderWitnessLine(line) {
|
|
1688
|
+
let text = `Witness: ${WITNESS_STATES[line.state]}`;
|
|
1689
|
+
if (line.reasons.length > 0)
|
|
1690
|
+
text += `: ${line.reasons.join("; ")}`;
|
|
1691
|
+
if (line.services.length > 0)
|
|
1692
|
+
text += ` (answered: ${line.services.join(", ")})`;
|
|
1693
|
+
return text;
|
|
1318
1694
|
}
|