@mikeargento/bitgraph-audit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/audit.d.ts +8 -6
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +28 -8
- package/dist/audit.js.map +1 -1
- package/dist/cli.js +38 -3
- package/dist/cli.js.map +1 -1
- package/dist/exports.d.ts +39 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +371 -0
- package/dist/exports.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +179 -19
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +15 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.d.ts.map +1 -1
- package/dist/report-md.js +222 -0
- package/dist/report-md.js.map +1 -1
- package/dist/types.d.ts +213 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/audit.ts +30 -8
- package/src/cli.ts +38 -3
- package/src/exports.ts +478 -0
- package/src/index.ts +12 -0
- package/src/ingest.ts +192 -18
- package/src/report-json.ts +15 -0
- package/src/report-md.ts +246 -0
- package/src/types.ts +203 -2
package/src/report-md.ts
CHANGED
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
* remainder line; the JSON report always carries the complete data.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { exportRunClaims } from "./exports.js";
|
|
24
25
|
import { buildJsonReport } from "./report-json.js";
|
|
25
26
|
import type {
|
|
26
27
|
AnomalyCode,
|
|
27
28
|
AuditJsonReport,
|
|
28
29
|
AuditResult,
|
|
29
30
|
DivergenceRecord,
|
|
31
|
+
ExportCheck,
|
|
32
|
+
ExportClaimRecord,
|
|
33
|
+
ExportCoveredFile,
|
|
34
|
+
ExportRun,
|
|
30
35
|
ReportAnomaly,
|
|
31
36
|
ReportPartition,
|
|
32
37
|
ReportProofRecord,
|
|
@@ -355,6 +360,32 @@ function executiveSummary(
|
|
|
355
360
|
lines.push("");
|
|
356
361
|
}
|
|
357
362
|
|
|
363
|
+
// Exports (bitgraph-export/1), only when the bundle carries any.
|
|
364
|
+
const ex = report.exports;
|
|
365
|
+
if (ex && ex.checks.length > 0) {
|
|
366
|
+
lines.push("### Exports (bitgraph-export/1)");
|
|
367
|
+
lines.push("");
|
|
368
|
+
lines.push(
|
|
369
|
+
`${withCommas(ex.checks.length)} export ${plural(ex.checks.length, "file was", "files were")} found. ` +
|
|
370
|
+
"An export is one JSON file that, with the file it covers, checks a tree/1 BitGraph on its own: " +
|
|
371
|
+
"the signed proof and its attestation, the tree's root document, the member's path or the owner's " +
|
|
372
|
+
"whole list, and the time evidence. Each was checked with verifyExport from bitgraph-verify, once " +
|
|
373
|
+
"for every file in this bundle it covers (matched by SHA-256 to a leaf's committed bytes or to its " +
|
|
374
|
+
"original), or once without a file when the bundle holds none. A claim the export does not carry, " +
|
|
375
|
+
"or one about a file that is not here, reads NOT_CARRIED and is never a failure. Any FALSE claim " +
|
|
376
|
+
"fails the audit (exit bit 1), as a bad proof does; the attestation claims count, because an export " +
|
|
377
|
+
"is checked as one self-contained object."
|
|
378
|
+
);
|
|
379
|
+
lines.push("");
|
|
380
|
+
for (const e of ex.checks) lines.push(`- ${exportSummaryLine(e)}`);
|
|
381
|
+
lines.push("");
|
|
382
|
+
lines.push(
|
|
383
|
+
"Each export's claims, the files it covers and its three time claims (the floor, the ceiling on " +
|
|
384
|
+
"Base, the ceiling on Ethereum, stated separately and never merged) are in the details below."
|
|
385
|
+
);
|
|
386
|
+
lines.push("");
|
|
387
|
+
}
|
|
388
|
+
|
|
358
389
|
// External time evidence.
|
|
359
390
|
lines.push("### External time evidence");
|
|
360
391
|
lines.push("");
|
|
@@ -525,6 +556,7 @@ function detailSections(
|
|
|
525
556
|
anchorDetails(lines, report);
|
|
526
557
|
witnessDetails(lines, report);
|
|
527
558
|
temporalDetails(lines, report);
|
|
559
|
+
exportDetails(lines, report);
|
|
528
560
|
attestationDetails(lines, report);
|
|
529
561
|
unsupportedVersionDetails(lines, report);
|
|
530
562
|
manifestDetails(lines, report);
|
|
@@ -838,6 +870,214 @@ function boundLine(bound: SegmentBound): string {
|
|
|
838
870
|
);
|
|
839
871
|
}
|
|
840
872
|
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
// Exports (bitgraph-export/1)
|
|
875
|
+
// ---------------------------------------------------------------------------
|
|
876
|
+
|
|
877
|
+
/** One summary line for an export: its verdict, path, kind and covered files. */
|
|
878
|
+
function exportSummaryLine(e: ExportCheck): string {
|
|
879
|
+
if (e.status === "malformed") {
|
|
880
|
+
// It says bitgraph-export/1 and is not one: a failed check, not an unknown format.
|
|
881
|
+
return `FALSE ${inlineCode(e.path)}: malformed, ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
|
|
882
|
+
}
|
|
883
|
+
if (e.status !== "checked") {
|
|
884
|
+
return `NOT CHECKED ${inlineCode(e.path)}: ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
|
|
885
|
+
}
|
|
886
|
+
const withFile = e.runs.filter((r) => r.file !== null).length;
|
|
887
|
+
const covered =
|
|
888
|
+
e.files.length === 0
|
|
889
|
+
? "no covered file in this bundle (checked without one)"
|
|
890
|
+
: `${withCommas(e.files.length)} covered ${plural(e.files.length, "file", "files")} in this bundle` +
|
|
891
|
+
(withFile < e.files.length ? `, ${withCommas(withFile)} checked one by one` : "");
|
|
892
|
+
const failed = e.failedClaims.length > 0 ? ` FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.` : "";
|
|
893
|
+
return `${e.verdict} ${inlineCode(e.path)}: ${exportKindPhrase(e)}; ${covered}.${failed}`;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function exportKindPhrase(e: ExportCheck): string {
|
|
897
|
+
const count = exportLeafCount(e);
|
|
898
|
+
const tree = count !== null ? ` of a tree of ${withCommas(count)} ${plural(count, "leaf", "leaves")}` : "";
|
|
899
|
+
if (e.kind === "member") return `member export${tree}`;
|
|
900
|
+
if (e.kind === "owner") return `the owner's export (the whole list)${tree}`;
|
|
901
|
+
return `an export with neither member evidence nor the owner's list${tree}`;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** The tree size the export's runs established (from the member, else from tree.root's detail). */
|
|
905
|
+
function exportLeafCount(e: ExportCheck): number | null {
|
|
906
|
+
for (const r of e.runs) if (r.member !== null) return r.member.count;
|
|
907
|
+
for (const c of [...e.claims, ...e.runs.flatMap((r) => r.claims)]) {
|
|
908
|
+
const m = c.id === "tree.root" && c.result === "TRUE" ? /a tree of (\d+) leaves/.exec(c.detail) : null;
|
|
909
|
+
if (m) return Number(m[1]);
|
|
910
|
+
}
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function exportFileLabel(f: ExportCoveredFile): string {
|
|
915
|
+
return f.paths.map(inlineCode).join(", ");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function exportDetails(lines: string[], report: AuditJsonReport): void {
|
|
919
|
+
const ex = report.exports;
|
|
920
|
+
if (ex === undefined || ex.checks.length === 0) return;
|
|
921
|
+
lines.push("### Exports (bitgraph-export/1)");
|
|
922
|
+
lines.push("");
|
|
923
|
+
lines.push(
|
|
924
|
+
"Each export below was checked with verifyExport from bitgraph-verify. Every claim is TRUE, FALSE, " +
|
|
925
|
+
"UNDETERMINED or NOT_CARRIED, with what it rests on, exactly as the verifier states it. Offline claims " +
|
|
926
|
+
"take block headers as the ones matching their hashes; whether each block is its chain's own is a " +
|
|
927
|
+
"confirmed claim, which this audit never looks up (it makes no network call), so those read " +
|
|
928
|
+
"UNDETERMINED (not checked). The proof inside an export is also in the proof analysis above under its " +
|
|
929
|
+
"canonical hash, where it counts as observed without artifact bytes: its artifact is the tree's " +
|
|
930
|
+
"84-byte root document, which travels inside the export and is checked here (claim tree.root)."
|
|
931
|
+
);
|
|
932
|
+
lines.push("");
|
|
933
|
+
for (const e of ex.checks) exportDetail(lines, e);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function exportDetail(lines: string[], e: ExportCheck): void {
|
|
937
|
+
lines.push(`#### Export ${inlineCode(e.path)}: ${e.status === "checked" ? e.verdict : e.status === "malformed" ? "FALSE (malformed)" : "NOT CHECKED"}`);
|
|
938
|
+
lines.push("");
|
|
939
|
+
if (e.status !== "checked") {
|
|
940
|
+
lines.push(
|
|
941
|
+
`Rejected at ingest: ${e.reason ?? e.status}. The file is not checked and is never treated as an ` +
|
|
942
|
+
"artifact; it fails the audit (exit bit 1), as an unsupported proof version does."
|
|
943
|
+
);
|
|
944
|
+
lines.push("");
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const facts: string[] = [`${exportKindPhrase(e).replace(/^./, (c) => c.toUpperCase())}.`];
|
|
948
|
+
if (e.proofHash !== undefined) facts.push(`Proof ${inlineCode(e.proofHash)}.`);
|
|
949
|
+
if (e.failedClaims.length > 0) facts.push(`FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.`);
|
|
950
|
+
lines.push(facts.join(" "));
|
|
951
|
+
lines.push("");
|
|
952
|
+
|
|
953
|
+
// The files it covers.
|
|
954
|
+
if (e.files.length === 0) {
|
|
955
|
+
lines.push(
|
|
956
|
+
"No file in this bundle is covered by this export, so it was checked once without a file: the claims " +
|
|
957
|
+
"about the file read NOT_CARRIED, which is not a failure."
|
|
958
|
+
);
|
|
959
|
+
lines.push("");
|
|
960
|
+
} else {
|
|
961
|
+
lines.push("Covered files in this bundle (matched by SHA-256):");
|
|
962
|
+
lines.push("");
|
|
963
|
+
lines.push(
|
|
964
|
+
...table(
|
|
965
|
+
["File", "SHA-256", "Leaf", "Placement", "Matched as", "Name in the export (unsigned)"],
|
|
966
|
+
e.files.slice(0, MAX_TABLE_ROWS).map((f) => [
|
|
967
|
+
exportFileLabel(f),
|
|
968
|
+
inlineCode(f.sha256Hex),
|
|
969
|
+
f.leafIndex !== null ? String(f.leafIndex) : "",
|
|
970
|
+
f.placement ?? "",
|
|
971
|
+
f.matchedAs === "committed-bytes" ? "committed bytes" : f.matchedAs === "original" ? "original" : "as is",
|
|
972
|
+
f.nameInExport ?? "",
|
|
973
|
+
])
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
if (e.files.length > MAX_TABLE_ROWS) lines.push("", `${withCommas(e.files.length - MAX_TABLE_ROWS)} more in the JSON report.`);
|
|
977
|
+
lines.push("");
|
|
978
|
+
if (e.runMode === "member-from-list") {
|
|
979
|
+
lines.push(
|
|
980
|
+
"The owner's list was checked once (claim tree.leaves); each file's member evidence was then derived " +
|
|
981
|
+
"from it exactly as verifyExport derives it, and verifyExport ran once per file with that evidence."
|
|
982
|
+
);
|
|
983
|
+
lines.push("");
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
for (const u of e.unchecked ?? []) lines.push(`- ${exportFileLabel(u.file)}: ${u.reason}.`);
|
|
987
|
+
if ((e.unchecked ?? []).length > 0) lines.push("");
|
|
988
|
+
|
|
989
|
+
// The three time claims, each on its own evidence, never merged.
|
|
990
|
+
const firstClaims = exportRunClaims(e, e.runs[0]!);
|
|
991
|
+
const why = (id: string): string => {
|
|
992
|
+
const c = firstClaims.find((x) => x.id === id);
|
|
993
|
+
return c ? `${inlineCode(id)} ${c.result}: ${c.detail}` : `${inlineCode(id)} was not reached`;
|
|
994
|
+
};
|
|
995
|
+
// Whether a block is its chain's own: the confirmed claim, which only an embedder's lookup answers.
|
|
996
|
+
const confirmedNote = (id: string, chain: string): string => {
|
|
997
|
+
const c = firstClaims.find((x) => x.id === id);
|
|
998
|
+
if (c === undefined || c.result === "UNDETERMINED") return ` Whether the block is ${chain}'s own is a confirmed claim, not looked up here.`;
|
|
999
|
+
return c.result === "TRUE"
|
|
1000
|
+
? ` The caller's own lookup confirms the block is ${chain}'s (${inlineCode(id)} TRUE).`
|
|
1001
|
+
: ` The caller's own lookup says the block is NOT ${chain}'s (${inlineCode(id)} ${c.result}: ${c.detail}).`;
|
|
1002
|
+
};
|
|
1003
|
+
lines.push("Time claims, each on its own evidence and never merged:");
|
|
1004
|
+
lines.push("");
|
|
1005
|
+
const t = e.times;
|
|
1006
|
+
lines.push(
|
|
1007
|
+
t.floor !== null
|
|
1008
|
+
? `- Floor: Ethereum block ${withCommas(t.floor.blockNumber)}, mined at ${formatTimestamp(t.floor.blockTimestamp)}, ` +
|
|
1009
|
+
`hash ${inlineCode(t.floor.blockHash)}: the proof's signed floor block, its header checked by hash. ` +
|
|
1010
|
+
"Every file in the tree was recorded after it (the record floor). Committed bytes that carry this position's " +
|
|
1011
|
+
"commitment were also finished after it (the content floor); an original inside them, and a file recorded as is, " +
|
|
1012
|
+
"are not dated by it. What the floor covers for each file is stated per run below." +
|
|
1013
|
+
confirmedNote("confirmed.floor", "Ethereum")
|
|
1014
|
+
: `- Floor: not established (${why("floor.header")}).`
|
|
1015
|
+
);
|
|
1016
|
+
lines.push(
|
|
1017
|
+
t.ceilingBase !== null
|
|
1018
|
+
? `- Ceiling on Base: the record existed by Base block ${withCommas(t.ceilingBase.blockNumber)}, at ` +
|
|
1019
|
+
`${formatTimestamp(t.ceilingBase.blockTimestamp)}, hash ${inlineCode(t.ceilingBase.blockHash)}. ` +
|
|
1020
|
+
(!t.ceilingBase.provisional
|
|
1021
|
+
? "The caller's own lookup confirmed the block against Base, so the time is no longer provisional."
|
|
1022
|
+
: firstClaims.some((x) => x.id === "confirmed.ceiling.base" && x.result === "FALSE")
|
|
1023
|
+
? `PROVISIONAL, and the caller's own lookup says the block is NOT Base's (${inlineCode("confirmed.ceiling.base")} FALSE).`
|
|
1024
|
+
: "PROVISIONAL: this time holds once the block is checked against Base, which this offline audit does not do.")
|
|
1025
|
+
: `- Ceiling on Base: not established (${why("ceiling.base")}).`
|
|
1026
|
+
);
|
|
1027
|
+
lines.push(
|
|
1028
|
+
t.ceilingEthereum !== null
|
|
1029
|
+
? `- Ceiling on Ethereum: the record existed by Ethereum block ${withCommas(t.ceilingEthereum.blockNumber)}, at ` +
|
|
1030
|
+
`${formatTimestamp(t.ceilingEthereum.blockTimestamp)}, hash ${inlineCode(t.ceilingEthereum.blockHash)}, ` +
|
|
1031
|
+
"through Base's output root, whatever Base's claim turns out to be." +
|
|
1032
|
+
confirmedNote("confirmed.ceiling.ethereum", "Ethereum")
|
|
1033
|
+
: `- Ceiling on Ethereum: not established (${why("ceiling.ethereum")}).`
|
|
1034
|
+
);
|
|
1035
|
+
lines.push("");
|
|
1036
|
+
|
|
1037
|
+
// Claims: the ones every run states alike once, then each run's own (the ones about its file).
|
|
1038
|
+
lines.push(e.runs.length > 1 ? "Claims, the same in every run:" : "Claims:");
|
|
1039
|
+
lines.push("");
|
|
1040
|
+
lines.push(...table(["Claim", "Result", "Level", "Rests on", "Detail"], e.claims.map((c) => claimRow(c))));
|
|
1041
|
+
lines.push("");
|
|
1042
|
+
const perFile: string[][] = [];
|
|
1043
|
+
let perFileTotal = 0;
|
|
1044
|
+
for (const r of e.runs) {
|
|
1045
|
+
for (const c of r.claims) {
|
|
1046
|
+
perFileTotal++;
|
|
1047
|
+
if (perFile.length < MAX_TABLE_ROWS) perFile.push([r.file !== null ? exportFileLabel(r.file) : "(no file)", ...claimRow(c)]);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (perFileTotal > 0) {
|
|
1051
|
+
lines.push("Claims that differ by file:");
|
|
1052
|
+
lines.push("");
|
|
1053
|
+
lines.push(...table(["File", "Claim", "Result", "Level", "Rests on", "Detail"], perFile));
|
|
1054
|
+
if (perFileTotal > MAX_TABLE_ROWS) lines.push("", `${withCommas(perFileTotal - MAX_TABLE_ROWS)} more rows in the JSON report.`);
|
|
1055
|
+
lines.push("");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Per run: the verdict, what the floor covers, and the verifier's own reading.
|
|
1059
|
+
lines.push(e.runs.length > 1 ? "Runs:" : "Run:");
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
for (const r of e.runs.slice(0, MAX_TABLE_ROWS)) lines.push(`- ${exportRunLine(r)}`);
|
|
1062
|
+
if (e.runs.length > MAX_TABLE_ROWS) lines.push(`- ${withCommas(e.runs.length - MAX_TABLE_ROWS)} more runs in the JSON report.`);
|
|
1063
|
+
lines.push("");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function claimRow(c: ExportClaimRecord): string[] {
|
|
1067
|
+
return [inlineCode(c.id), c.result, c.level, c.restsOn, c.detail];
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function exportRunLine(r: ExportRun): string {
|
|
1071
|
+
const who = r.file !== null ? exportFileLabel(r.file) : "Without a file";
|
|
1072
|
+
const floor =
|
|
1073
|
+
r.floorCovers === "content"
|
|
1074
|
+
? " Recorded after the floor block, and its committed bytes were finished after it; an original inside them is not dated by it."
|
|
1075
|
+
: r.floorCovers === "record"
|
|
1076
|
+
? " Recorded as is: recorded after the floor block; the bytes themselves are not dated."
|
|
1077
|
+
: "";
|
|
1078
|
+
return `${who}: ${r.verdict}.${floor} Reading: ${r.reading.length > 0 ? r.reading : "(none)"}`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
841
1081
|
function attestationDetails(lines: string[], report: AuditJsonReport): void {
|
|
842
1082
|
lines.push("### Attestation breakdown");
|
|
843
1083
|
lines.push("");
|
|
@@ -1048,6 +1288,12 @@ function codeMeaning(code: AnomalyCode): string {
|
|
|
1048
1288
|
return "A validated attestation document's measurement does not equal the measurement the proof declares.";
|
|
1049
1289
|
case "attestation-user-data-mismatch":
|
|
1050
1290
|
return "A validated attestation document is not bound to the specific proof that carries it.";
|
|
1291
|
+
case "export-malformed":
|
|
1292
|
+
return "A file declares the bitgraph-export/1 format but lacks an export's structure (a proof and the tree's root document). It was not checked, and it fails the audit.";
|
|
1293
|
+
case "export-unsupported-format":
|
|
1294
|
+
return "A file declares a bitgraph-export format this audit does not support (only bitgraph-export/1). It was not checked, and it fails the audit.";
|
|
1295
|
+
case "export-too-large":
|
|
1296
|
+
return "A file opens an export but is larger than this audit reads for one export. It was not checked, and it fails the audit.";
|
|
1051
1297
|
default:
|
|
1052
1298
|
return "See the anomaly details section.";
|
|
1053
1299
|
}
|
package/src/types.ts
CHANGED
|
@@ -123,6 +123,13 @@ export type AnomalyCode =
|
|
|
123
123
|
| "attestation-measurement-mismatch"
|
|
124
124
|
/** A validated attestation document's user_data is not bound to this proof's canonical proof hash. */
|
|
125
125
|
| "attestation-user-data-mismatch"
|
|
126
|
+
// --- Exports (bitgraph-export/1) ---
|
|
127
|
+
/** A file declares format "bitgraph-export/1" but lacks an export's structure (a proof object and tree.rootDocument). Not checked; fails the audit (exit bit 1). */
|
|
128
|
+
| "export-malformed"
|
|
129
|
+
/** A file declares a bitgraph-export format other than "bitgraph-export/1". Not checked; fails the audit (exit bit 1), as an unsupported proof version does. */
|
|
130
|
+
| "export-unsupported-format"
|
|
131
|
+
/** A file opens an export object but is larger than the audit's cap for one export's JSON (IngestLimits.maxExportJsonBytes). Not checked; fails the audit (exit bit 1). */
|
|
132
|
+
| "export-too-large"
|
|
126
133
|
| (string & {});
|
|
127
134
|
|
|
128
135
|
/**
|
|
@@ -408,6 +415,8 @@ export interface IngestCounts {
|
|
|
408
415
|
witnesses: number;
|
|
409
416
|
/** Ceiling files (bitgraph-ceiling/1) and ceiling status notes (bitgraph-ceiling-status/1). */
|
|
410
417
|
ceilings?: number;
|
|
418
|
+
/** Export-shaped files (format "bitgraph-export/..."), checked or rejected. */
|
|
419
|
+
exports?: number;
|
|
411
420
|
/** Container entries skipped for unsafe paths. */
|
|
412
421
|
skippedUnsafePaths: number;
|
|
413
422
|
}
|
|
@@ -437,6 +446,14 @@ export interface IngestLimits {
|
|
|
437
446
|
* declaring a larger size aborts ingest before any allocation.
|
|
438
447
|
*/
|
|
439
448
|
maxMetadataEntryBytes: number;
|
|
449
|
+
/**
|
|
450
|
+
* Ceiling on one export's JSON (bitgraph-export/1), for every container,
|
|
451
|
+
* directories included. Other JSON candidates stop at 8 MiB; an entry that
|
|
452
|
+
* opens an export object may run to this size (the owner's export of a
|
|
453
|
+
* large tree lists every leaf). A larger export is reported
|
|
454
|
+
* (export-too-large) and not checked. Default 192 MiB.
|
|
455
|
+
*/
|
|
456
|
+
maxExportJsonBytes?: number;
|
|
440
457
|
}
|
|
441
458
|
|
|
442
459
|
/**
|
|
@@ -473,6 +490,11 @@ export interface IngestResult {
|
|
|
473
490
|
ceilings?: CeilingFile[];
|
|
474
491
|
/** Ceiling status notes (bitgraph-ceiling-status/1): a package saying why a ceiling is absent. */
|
|
475
492
|
ceilingStatuses?: CeilingFile[];
|
|
493
|
+
/**
|
|
494
|
+
* Export-shaped files (format "bitgraph-export/..."), in observation order.
|
|
495
|
+
* Evidence, never artifacts. Optional so older embedders' IngestResults still type.
|
|
496
|
+
*/
|
|
497
|
+
exports?: ExportFile[];
|
|
476
498
|
/** Present when a root manifest.json entry existed. */
|
|
477
499
|
manifest?: ManifestReport;
|
|
478
500
|
|
|
@@ -1314,6 +1336,8 @@ export interface AuditOptions {
|
|
|
1314
1336
|
trustedRootCaDer?: Uint8Array;
|
|
1315
1337
|
/** Ceilings in time: the declared writer, the chain, and an optional Base RPC for the one online check. */
|
|
1316
1338
|
ceilings?: import("./ceilings.js").CeilingAuditOptions;
|
|
1339
|
+
/** Exports (bitgraph-export/1): extra spec hashes, PCR0 pins, and optional lookups for embedders that allow network. */
|
|
1340
|
+
exports?: import("./exports.js").ExportAuditOptions;
|
|
1317
1341
|
}
|
|
1318
1342
|
|
|
1319
1343
|
/**
|
|
@@ -1351,7 +1375,7 @@ export interface CeilingCheck {
|
|
|
1351
1375
|
*/
|
|
1352
1376
|
status: "verified" | "failed" | "pending" | "unmatched";
|
|
1353
1377
|
reason?: string;
|
|
1354
|
-
/** One line for people, e.g. "Ceiling: Base block N at hh:mm:ss UTC
|
|
1378
|
+
/** One line for people, e.g. "Ceiling: included in Base block N at hh:mm:ss UTC. ..."; it never claims settlement the file does not prove. */
|
|
1355
1379
|
label?: string;
|
|
1356
1380
|
window?: {
|
|
1357
1381
|
floorBlock: number | null;
|
|
@@ -1406,6 +1430,160 @@ export interface CeilingAnalysis {
|
|
|
1406
1430
|
statuses: Array<{ path: string; status: string; note: string }>;
|
|
1407
1431
|
}
|
|
1408
1432
|
|
|
1433
|
+
// ---------------------------------------------------------------------------
|
|
1434
|
+
// Exports (bitgraph-export/1)
|
|
1435
|
+
// ---------------------------------------------------------------------------
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* An export-shaped file as found in the bundle: a JSON object whose format
|
|
1439
|
+
* field starts with "bitgraph-export/". Found by structure, never by name.
|
|
1440
|
+
* Evidence, never an artifact.
|
|
1441
|
+
*/
|
|
1442
|
+
export interface ExportFile {
|
|
1443
|
+
path: string;
|
|
1444
|
+
fileSha256Hex: string;
|
|
1445
|
+
/** The format the file declares. */
|
|
1446
|
+
format: string;
|
|
1447
|
+
/**
|
|
1448
|
+
* ok: a bitgraph-export/1 document (bitgraph-verify's parseExport accepts it).
|
|
1449
|
+
* malformed: declares bitgraph-export/1 but lacks a proof object or tree.rootDocument.
|
|
1450
|
+
* unsupported-format: declares another bitgraph-export version.
|
|
1451
|
+
* too-large: opens an export object but is larger than the audit's export JSON cap; not parsed (json is empty).
|
|
1452
|
+
*/
|
|
1453
|
+
status: "ok" | "malformed" | "unsupported-format" | "too-large";
|
|
1454
|
+
json: Record<string, unknown>;
|
|
1455
|
+
/** Canonical hash of the proof the export carries, when that proof joined the proof analysis (a proof-shaped bitgraph/1 object). */
|
|
1456
|
+
proofHash?: string;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/** One claim exactly as bitgraph-verify's verifyExport states it. */
|
|
1460
|
+
export interface ExportClaimRecord {
|
|
1461
|
+
/** Stable id, dotted: "proof.signature", "tree.member", "bytes.floor", "ceiling.base", "confirmed.floor" ... */
|
|
1462
|
+
id: string;
|
|
1463
|
+
name: string;
|
|
1464
|
+
result: "TRUE" | "FALSE" | "UNDETERMINED" | "NOT_CARRIED";
|
|
1465
|
+
/** What the result rests on (the primitive or the evidence), empty when nothing was checked. */
|
|
1466
|
+
restsOn: string;
|
|
1467
|
+
detail: string;
|
|
1468
|
+
/** offline: recomputed from the bytes in hand. confirmed: from the caller's own chain lookups (never made by the CLI). */
|
|
1469
|
+
level: "offline" | "confirmed";
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** The three time claims as verifyExport establishes them. Never merged; a null field was not established. */
|
|
1473
|
+
export interface ExportTimes {
|
|
1474
|
+
/** The committed bytes were finished after this Ethereum block (the proof's signed floor block, its header checked by hash). */
|
|
1475
|
+
floor: { blockNumber: number; blockHash: string; blockTimestamp: number } | null;
|
|
1476
|
+
/** The record existed by this Base block, at its time. Provisional until the block is checked against Base (offline, always provisional). */
|
|
1477
|
+
ceilingBase: { blockNumber: number; blockHash: string; blockTimestamp: number; provisional: boolean } | null;
|
|
1478
|
+
/** The record existed by this Ethereum block (through Base's output root; needs no trust in Base). */
|
|
1479
|
+
ceilingEthereum: { blockNumber: number; blockHash: string; blockTimestamp: number } | null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/** A file in the bundle that an export covers, matched by SHA-256 against a leaf's digests. */
|
|
1483
|
+
export interface ExportCoveredFile {
|
|
1484
|
+
/** SHA-256 of the file, lowercase hex. */
|
|
1485
|
+
sha256Hex: string;
|
|
1486
|
+
/** Every bundle path holding these bytes. */
|
|
1487
|
+
paths: string[];
|
|
1488
|
+
byteLength: number;
|
|
1489
|
+
/** The leaf it matched, in tree order: the first leaf whose committed or original digest is the file's, as verifyExport picks it. */
|
|
1490
|
+
leafIndex: number | null;
|
|
1491
|
+
/** That leaf's placement: "as-is", "trailer/1", "container/1" or "container/2". */
|
|
1492
|
+
placement: string | null;
|
|
1493
|
+
/**
|
|
1494
|
+
* committed-bytes: the file is the leaf's committed bytes (its artifact digest).
|
|
1495
|
+
* original: the file is the original the committed bytes were made from (its origin digest).
|
|
1496
|
+
* as-is: the leaf records the file exactly as it is (the two digests are the same).
|
|
1497
|
+
*/
|
|
1498
|
+
matchedAs: "committed-bytes" | "original" | "as-is";
|
|
1499
|
+
/** The name an owner's export lists for that leaf. Unsigned and informational. */
|
|
1500
|
+
nameInExport?: string;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/** A claim of one run that is not common to every run of its export, with its place in verifyExport's order. */
|
|
1504
|
+
export interface ExportOwnClaim extends ExportClaimRecord {
|
|
1505
|
+
/** Index of this claim in the run's whole claim list, in verifyExport's order. */
|
|
1506
|
+
position: number;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* One verifyExport run: with one covered file, or once without a file when
|
|
1511
|
+
* the bundle holds none. The run's whole claim list, in verifyExport's
|
|
1512
|
+
* order, is the export's common claims (ExportCheck.claims) with this run's
|
|
1513
|
+
* own claims put back at their positions: exportRunClaims(check, run).
|
|
1514
|
+
* verifyExport's reasons are that list's FALSE and UNDETERMINED claims, and
|
|
1515
|
+
* its time claims are the export's (ExportCheck.times): they do not depend
|
|
1516
|
+
* on the file.
|
|
1517
|
+
*/
|
|
1518
|
+
export interface ExportRun {
|
|
1519
|
+
/** The covered file this run checked; null for the run without a file. */
|
|
1520
|
+
file: ExportCoveredFile | null;
|
|
1521
|
+
/** verifyExport's verdict over its offline claims. */
|
|
1522
|
+
verdict: "TRUE" | "FALSE" | "UNDETERMINED";
|
|
1523
|
+
/** This run's claims that differ from the export's common claims (in practice the ones about its file: tree.member, bytes.member, bytes.floor). */
|
|
1524
|
+
claims: ExportOwnClaim[];
|
|
1525
|
+
/** The member the run established (index, count, placement, digests), when it did. */
|
|
1526
|
+
member: { index: number; count: number; placement: string; artifactHex: string; originHex: string } | null;
|
|
1527
|
+
/**
|
|
1528
|
+
* What the floor covers for this run's file, read from its claims: content
|
|
1529
|
+
* (the committed bytes carry the commitment, so they were finished after the
|
|
1530
|
+
* floor block; an original inside them is not dated by it), record (an as-is
|
|
1531
|
+
* leaf: the record was made after the floor block, the bytes themselves are
|
|
1532
|
+
* not dated), or null (the file was not established as a member, or no file
|
|
1533
|
+
* was checked).
|
|
1534
|
+
*/
|
|
1535
|
+
floorCovers: "content" | "record" | null;
|
|
1536
|
+
/** verifyExport's plain-language reading, written from its claims. */
|
|
1537
|
+
reading: string;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
/** One export file, checked. */
|
|
1541
|
+
export interface ExportCheck {
|
|
1542
|
+
path: string;
|
|
1543
|
+
fileSha256Hex: string;
|
|
1544
|
+
format: string;
|
|
1545
|
+
/** checked: verifyExport ran. malformed / unsupported-format / too-large: rejected at ingest, not checked (exit bit 1). */
|
|
1546
|
+
status: "checked" | "malformed" | "unsupported-format" | "too-large";
|
|
1547
|
+
/** Why the file was not checked. */
|
|
1548
|
+
reason?: string;
|
|
1549
|
+
/** member: carries one member's evidence (tree.member). owner: carries the whole list (tree.leaves). root-only: neither. */
|
|
1550
|
+
kind?: "member" | "owner" | "root-only";
|
|
1551
|
+
/** Canonical hash of the export's proof, which also appears in the proof analysis. */
|
|
1552
|
+
proofHash?: string;
|
|
1553
|
+
/**
|
|
1554
|
+
* The audit's judgment of the export: FALSE when any run's verdict is FALSE
|
|
1555
|
+
* or any claim of any run is FALSE (a confirmed claim included, from an
|
|
1556
|
+
* embedder's lookup); else UNDETERMINED when any run is; else TRUE. A FALSE
|
|
1557
|
+
* export fails the audit (exit bit 1), as a bad proof does.
|
|
1558
|
+
*/
|
|
1559
|
+
verdict: "TRUE" | "FALSE" | "UNDETERMINED";
|
|
1560
|
+
/** Ids of the claims that are FALSE in any run, in first-seen order. */
|
|
1561
|
+
failedClaims: string[];
|
|
1562
|
+
/** The three time claims, never merged. They do not depend on the file, so every run states the same; read from the first. */
|
|
1563
|
+
times: ExportTimes;
|
|
1564
|
+
/** The claims every run states alike (same id, result, level, restsOn and detail), in verifyExport's order. With one run, all of its claims. */
|
|
1565
|
+
claims: ExportClaimRecord[];
|
|
1566
|
+
/** Files in the bundle this export covers, sorted by first path. */
|
|
1567
|
+
files: ExportCoveredFile[];
|
|
1568
|
+
/** One run per covered file (in the order of files), or one run without a file when the bundle holds none. */
|
|
1569
|
+
runs: ExportRun[];
|
|
1570
|
+
/**
|
|
1571
|
+
* How the per-file runs of an owner's export were made. as-given: verifyExport
|
|
1572
|
+
* on the export itself, once per file. member-from-list: the owner's list was
|
|
1573
|
+
* checked once (claim tree.leaves TRUE), then each file's member evidence was
|
|
1574
|
+
* derived from it exactly as verifyExport derives it, verifyExport ran on that
|
|
1575
|
+
* member form, and the list claim was put back in its place; the runs equal
|
|
1576
|
+
* the as-given runs, without rebuilding the whole tree once per file.
|
|
1577
|
+
*/
|
|
1578
|
+
runMode?: "as-given" | "member-from-list";
|
|
1579
|
+
/** Covered files not checked one by one: only when an owner's list fails and the per-file budget ran out, or a file could not be re-read. */
|
|
1580
|
+
unchecked?: Array<{ file: ExportCoveredFile; reason: string }>;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
export interface ExportAnalysis {
|
|
1584
|
+
checks: ExportCheck[];
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1409
1587
|
export interface AuditResult {
|
|
1410
1588
|
runMetadata: AuditRunMetadata;
|
|
1411
1589
|
ingest: IngestResult;
|
|
@@ -1419,6 +1597,8 @@ export interface AuditResult {
|
|
|
1419
1597
|
attestations: AttestationAnalysis;
|
|
1420
1598
|
/** Ceilings in time on Base. Absent from results made before this stage existed. */
|
|
1421
1599
|
ceilings?: CeilingAnalysis;
|
|
1600
|
+
/** Exports (bitgraph-export/1), each checked with verifyExport per covered file. Absent from results made before this stage existed. */
|
|
1601
|
+
exports?: ExportAnalysis;
|
|
1422
1602
|
}
|
|
1423
1603
|
|
|
1424
1604
|
/**
|
|
@@ -1426,7 +1606,13 @@ export interface AuditResult {
|
|
|
1426
1606
|
*
|
|
1427
1607
|
* bit 1 (value 1): verification failures. Set when any proof's canonical
|
|
1428
1608
|
* checks failed at either tier, or any proof-shaped input was rejected
|
|
1429
|
-
* as an unsupported version
|
|
1609
|
+
* as an unsupported version, or any export (bitgraph-export/1) has a
|
|
1610
|
+
* FALSE claim (its verdict is FALSE), or an export-shaped file was
|
|
1611
|
+
* rejected as malformed or of an unsupported format. An export's
|
|
1612
|
+
* attestation claims are part of its verdict: an export is checked as
|
|
1613
|
+
* one self-contained object, as a carrier is. NOT_CARRIED claims (a
|
|
1614
|
+
* pending ceiling, the covered file absent) never fail an export.
|
|
1615
|
+
* artifact-unavailable is NOT a failure: a
|
|
1430
1616
|
* proof without artifact bytes passes or fails on its bytes-free checks
|
|
1431
1617
|
* alone, unless a supplied trust policy makes those checks fail (for
|
|
1432
1618
|
* example requireSlot), in which case its status is "failed" and it
|
|
@@ -1616,6 +1802,19 @@ export interface ReportSummary {
|
|
|
1616
1802
|
segmentsUpperBounded: number;
|
|
1617
1803
|
segmentsUnanchored: number;
|
|
1618
1804
|
};
|
|
1805
|
+
/** Exports (bitgraph-export/1), as counts. Present only when the bundle carries export-shaped files. */
|
|
1806
|
+
exports?: {
|
|
1807
|
+
/** Export-shaped files found. */
|
|
1808
|
+
files: number;
|
|
1809
|
+
/** Exports checked, by the audit's verdict. */
|
|
1810
|
+
verdictTrue: number;
|
|
1811
|
+
verdictFalse: number;
|
|
1812
|
+
verdictUndetermined: number;
|
|
1813
|
+
/** Export-shaped files rejected (malformed or an unsupported format). */
|
|
1814
|
+
rejected: number;
|
|
1815
|
+
/** verifyExport runs made with a covered file from the bundle. */
|
|
1816
|
+
coveredFilesChecked: number;
|
|
1817
|
+
};
|
|
1619
1818
|
exit: ExitFlags;
|
|
1620
1819
|
}
|
|
1621
1820
|
|
|
@@ -1662,6 +1861,8 @@ export interface AuditJsonReport {
|
|
|
1662
1861
|
};
|
|
1663
1862
|
/** Ceilings in time on Base (bitgraph-ceiling/1). Absent when the bundle carries none. */
|
|
1664
1863
|
ceilings?: import("./types.js").CeilingAnalysis;
|
|
1864
|
+
/** Exports (bitgraph-export/1), one entry per export file in observation order. Absent when the bundle carries none. */
|
|
1865
|
+
exports?: ExportAnalysis;
|
|
1665
1866
|
attestations: {
|
|
1666
1867
|
records: ProofAttestationRecord[];
|
|
1667
1868
|
counts: AttestationAnalysis["counts"];
|