@mikeargento/bitgraph-audit 0.7.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/ceilings.d.ts +25 -0
- package/dist/ceilings.d.ts.map +1 -1
- package/dist/ceilings.js +159 -31
- package/dist/ceilings.js.map +1 -1
- package/dist/cli.js +40 -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 +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +225 -0
- package/dist/report-md.js.map +1 -1
- package/dist/settlement-blobs.d.ts +131 -0
- package/dist/settlement-blobs.d.ts.map +1 -0
- package/dist/settlement-blobs.js +682 -0
- package/dist/settlement-blobs.js.map +1 -0
- package/dist/types.d.ts +254 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/fixtures/settlement/0x018efc046e94610e2530344caf1574e507a3182e1a975fb25b032ec1434a3f5c.bin +0 -0
- package/src/__tests__/fixtures/settlement/ceiling-51979918.json +101 -0
- package/src/__tests__/fixtures/settlement/pointer-51979918.json +69 -0
- package/src/__tests__/settlement.test.ts +254 -0
- package/src/audit.ts +30 -8
- package/src/ceilings.ts +167 -33
- package/src/cli.ts +39 -3
- package/src/exports.ts +478 -0
- package/src/index.ts +16 -0
- package/src/ingest.ts +192 -18
- package/src/report-json.ts +15 -0
- package/src/report-md.ts +248 -0
- package/src/settlement-blobs.ts +689 -0
- package/src/types.ts +234 -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,
|
|
@@ -348,11 +353,39 @@ function executiveSummary(
|
|
|
348
353
|
} else {
|
|
349
354
|
lines.push(`- ${c.status.toUpperCase()} ${inlineCode(c.path)}: ${c.reason ?? ""}`);
|
|
350
355
|
}
|
|
356
|
+
// Settlement on Ethereum (bitgraph-settlement/1), one line per layer.
|
|
357
|
+
for (const line of c.settlement?.lines ?? []) lines.push(` - ${line}`);
|
|
351
358
|
}
|
|
352
359
|
for (const st of cl.statuses) lines.push(`- ${st.status} ${inlineCode(st.path)}: ${st.note}`);
|
|
353
360
|
lines.push("");
|
|
354
361
|
}
|
|
355
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
|
+
|
|
356
389
|
// External time evidence.
|
|
357
390
|
lines.push("### External time evidence");
|
|
358
391
|
lines.push("");
|
|
@@ -523,6 +556,7 @@ function detailSections(
|
|
|
523
556
|
anchorDetails(lines, report);
|
|
524
557
|
witnessDetails(lines, report);
|
|
525
558
|
temporalDetails(lines, report);
|
|
559
|
+
exportDetails(lines, report);
|
|
526
560
|
attestationDetails(lines, report);
|
|
527
561
|
unsupportedVersionDetails(lines, report);
|
|
528
562
|
manifestDetails(lines, report);
|
|
@@ -836,6 +870,214 @@ function boundLine(bound: SegmentBound): string {
|
|
|
836
870
|
);
|
|
837
871
|
}
|
|
838
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
|
+
|
|
839
1081
|
function attestationDetails(lines: string[], report: AuditJsonReport): void {
|
|
840
1082
|
lines.push("### Attestation breakdown");
|
|
841
1083
|
lines.push("");
|
|
@@ -1046,6 +1288,12 @@ function codeMeaning(code: AnomalyCode): string {
|
|
|
1046
1288
|
return "A validated attestation document's measurement does not equal the measurement the proof declares.";
|
|
1047
1289
|
case "attestation-user-data-mismatch":
|
|
1048
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.";
|
|
1049
1297
|
default:
|
|
1050
1298
|
return "See the anomaly details section.";
|
|
1051
1299
|
}
|