@mikeargento/bitgraph-audit 0.8.0 → 0.10.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.
Files changed (51) hide show
  1. package/README.md +8 -2
  2. package/dist/audit.d.ts +9 -6
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +34 -8
  5. package/dist/audit.js.map +1 -1
  6. package/dist/ceilings.d.ts.map +1 -1
  7. package/dist/ceilings.js +1 -0
  8. package/dist/ceilings.js.map +1 -1
  9. package/dist/cli.js +109 -21
  10. package/dist/cli.js.map +1 -1
  11. package/dist/exports.d.ts +39 -0
  12. package/dist/exports.d.ts.map +1 -0
  13. package/dist/exports.js +374 -0
  14. package/dist/exports.js.map +1 -0
  15. package/dist/floors.d.ts +23 -0
  16. package/dist/floors.d.ts.map +1 -0
  17. package/dist/floors.js +203 -0
  18. package/dist/floors.js.map +1 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/ingest.d.ts +2 -0
  24. package/dist/ingest.d.ts.map +1 -1
  25. package/dist/ingest.js +193 -19
  26. package/dist/ingest.js.map +1 -1
  27. package/dist/report-json.d.ts.map +1 -1
  28. package/dist/report-json.js +30 -0
  29. package/dist/report-json.js.map +1 -1
  30. package/dist/report-md.d.ts.map +1 -1
  31. package/dist/report-md.js +315 -25
  32. package/dist/report-md.js.map +1 -1
  33. package/dist/temporal.d.ts +12 -0
  34. package/dist/temporal.d.ts.map +1 -1
  35. package/dist/temporal.js +245 -41
  36. package/dist/temporal.js.map +1 -1
  37. package/dist/types.d.ts +348 -7
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +3 -2
  40. package/src/__tests__/base-floor.test.ts +405 -0
  41. package/src/audit.ts +36 -8
  42. package/src/ceilings.ts +1 -0
  43. package/src/cli.ts +117 -22
  44. package/src/exports.ts +482 -0
  45. package/src/floors.ts +242 -0
  46. package/src/index.ts +20 -0
  47. package/src/ingest.ts +207 -18
  48. package/src/report-json.ts +30 -0
  49. package/src/report-md.ts +353 -29
  50. package/src/temporal.ts +279 -48
  51. package/src/types.ts +341 -6
package/src/cli.ts CHANGED
@@ -21,10 +21,12 @@ import { readFileSync } from "node:fs";
21
21
  import { mkdir, mkdtemp, stat, writeFile } from "node:fs/promises";
22
22
  import { tmpdir } from "node:os";
23
23
  import { join, basename } from "node:path";
24
- import { parseCarrier, verifyCarrier } from "@mikeargento/bitgraph-verify";
25
- import type { VerificationPolicy } from "@mikeargento/bitgraph-verify";
24
+ import { CARRIER_VERSION_3, baseHeaderFields, parseCarrier, verifyCarrier } from "@mikeargento/bitgraph-verify";
25
+ import type { CarrierPayload, VerificationPolicy } from "@mikeargento/bitgraph-verify";
26
26
  import { auditToolVersion, computeExitFlags, runAudit } from "./audit.js";
27
27
  import { attestationTimestampMs } from "./attestation.js";
28
+ import { exportRunClaims } from "./exports.js";
29
+ import { FLOOR_HEADER_VERSION } from "./ingest.js";
28
30
  import { buildJsonReport } from "./report-json.js";
29
31
  import { buildMarkdownReport } from "./report-md.js";
30
32
  import type { ExitFlags } from "./types.js";
@@ -61,11 +63,35 @@ function helpText(): string {
61
63
  USAGE_LINE,
62
64
  "",
63
65
  "The bundle may be a directory, a .tar archive, a .tar.gz/.tgz,",
64
- "or a single bitgraph-carrier/1 file (a file with its proof inside):",
65
- "a carrier is unpacked and audited as the bundle it carries, and its",
66
- "own verdict (TRUE / FALSE / UNDETERMINED, with the time window) is",
67
- "printed first.",
68
- "archive. The audit runs entirely offline: no RPC, no HTTP, no DNS.",
66
+ "or a single BitGraphed file (bitgraph-carrier/1, /2 or /3: a file",
67
+ "with its proof inside): a carrier is unpacked and audited as the",
68
+ "bundle it carries, and its own verdict (TRUE / FALSE / UNDETERMINED,",
69
+ "with the time window) is printed first. The unpacked bundle holds the",
70
+ "committed bytes and proof.json, and the floor: for /1 and /2 the",
71
+ "Ethereum anchor proofs and their witnesses under ethereum-anchors/",
72
+ "(anchor-floor.json, anchor-floor.witness.json, and the closing",
73
+ "anchor's pair when the file carries it); for /3 the Base block the",
74
+ "proof signs as its floor, as base-floor/floor-header.json",
75
+ "(bitgraph-floor-header/1: chain, evmChainId, blockNumber, blockHash,",
76
+ "blockTimestamp, and the header's RLP as 0x hex). A /3 file has no",
77
+ "closing anchor: order after the record is the chain of proof hashes.",
78
+ "The audit runs entirely offline: no RPC, no HTTP, no DNS.",
79
+ "",
80
+ "Floors. A proof's floor is the block the enclave signs into it: an",
81
+ "Ethereum anchor (commit.slotAnchor, enclave v7 to v9) or a Base block",
82
+ "(commit.slotFloor, enclave v10). A Base floor bounds its proof not",
83
+ "before the block's time, never after anything. A floor header file",
84
+ "(bitgraph-floor-header/1) anywhere in the bundle is checked against",
85
+ "the floor a proof signs; without one, the time is the signed one and",
86
+ "confirming the block needs a Base lookup.",
87
+ "",
88
+ "Exports (bitgraph-export/1) anywhere in the bundle are found by their",
89
+ "format field. Each is checked with verifyExport once per file in the",
90
+ "bundle it covers (by SHA-256: a member's committed bytes or original;",
91
+ "for the owner's export, any leaf's), or once without a file when none",
92
+ "is there, and its proof joins the proof analysis. Every claim, the",
93
+ "covered files and the three time claims (floor; ceiling on Base,",
94
+ "provisional; ceiling on Ethereum) are reported, never merged.",
69
95
  "",
70
96
  "Options:",
71
97
  " --out <dir> Directory to write the report files into",
@@ -98,7 +124,12 @@ function helpText(): string {
98
124
  " are absent from the bundle is NOT a failure by itself: its",
99
125
  " bytes-free checks decide, unless a supplied trust policy makes",
100
126
  " them fail (for example requireSlot), in which case it counts",
101
- " here.",
127
+ " here. Also: an export (bitgraph-export/1) with any FALSE claim,",
128
+ " its attestation claims included (an export is checked as one",
129
+ " self-contained object, as a carrier is), or an export-shaped",
130
+ " file that is malformed, of an unsupported format, or too large",
131
+ " to read. A claim the export does not carry (NOT_CARRIED: a",
132
+ " pending ceiling, the covered file absent) is never a failure.",
102
133
  " 2 Chain or authority anomalies, divergences between valid proofs,",
103
134
  " or anchor witness verification failures: unexplained counter",
104
135
  " positions, chain breaks, collisions, cross-kind position reuse,",
@@ -108,6 +139,10 @@ function helpText(): string {
108
139
  " RLP malformation, an invalid candidate anchor, or an unmatched",
109
140
  " witness), or a ceiling in time (bitgraph-ceiling/1) that fails",
110
141
  " its check against its proof (wrong writer, root, block, header).",
142
+ " Also a floor problem: a proof that signs two floors (an Ethereum",
143
+ " anchor and a Base block), a Base floor that is malformed or off",
144
+ " Base mainnet's schedule, or a floor header file that contradicts",
145
+ " the floor a proof signs or matches none.",
111
146
  " A pending ceiling or a ceiling status note never sets a bit.",
112
147
  " Benign findings are reported but never set exit bits:",
113
148
  " duplicate copies, manifest advisories, unsafe paths, embedded",
@@ -122,7 +157,8 @@ function helpText(): string {
122
157
  "change the exit code on their own: an invalid attestation document on",
123
158
  "an otherwise verified proof is reported without affecting the exit",
124
159
  "code, and counts under exit bit 1 only when a supplied trust policy",
125
- "made verification itself fail.",
160
+ "made verification itself fail. The exception is an export's own",
161
+ "attestation claims, which are part of the export's verdict (exit 1).",
126
162
  ].join("\n");
127
163
  }
128
164
 
@@ -234,13 +270,52 @@ function loadTrustPolicy(path: string): VerificationPolicy {
234
270
  function exitMeaning(flags: ExitFlags): string {
235
271
  if (flags.code === 0) return "clean: no verification failures, no chain anomalies, no divergences";
236
272
  const parts: string[] = [];
237
- if (flags.verificationFailures) parts.push("verification failures");
273
+ if (flags.verificationFailures) parts.push("verification failures (a proof, or an export with a FALSE claim)");
238
274
  if (flags.chainAnomaliesOrDivergences) {
239
275
  parts.push("chain anomalies, divergences, or anchor witness verification failures (or a failed ceiling in time)");
240
276
  }
241
277
  return parts.join("; ");
242
278
  }
243
279
 
280
+ /**
281
+ * Write a carrier's floor into the unpacked bundle. /1 and /2: the Ethereum
282
+ * anchor proofs and witnesses under ethereum-anchors/, exactly as before.
283
+ * /3: the Base block the proof signs as its floor, as
284
+ * base-floor/floor-header.json (bitgraph-floor-header/1); no anchor proof
285
+ * stands under it and there is no closing anchor to write.
286
+ */
287
+ async function writeCarrierFloor(dir: string, payload: CarrierPayload): Promise<void> {
288
+ const floor = payload.floor;
289
+ if (floor.basis === "base-header") {
290
+ const fields = baseHeaderFields(floor.header);
291
+ const out = join(dir, "base-floor");
292
+ await mkdir(out);
293
+ await writeFile(
294
+ join(out, "floor-header.json"),
295
+ JSON.stringify(
296
+ {
297
+ version: FLOOR_HEADER_VERSION,
298
+ chain: "base",
299
+ evmChainId: 8453,
300
+ ...(fields !== null ? { blockNumber: fields.blockNumber, blockHash: fields.blockHash, blockTimestamp: fields.timestamp } : {}),
301
+ header: floor.header,
302
+ },
303
+ null,
304
+ 2
305
+ )
306
+ );
307
+ return;
308
+ }
309
+ const anchors = join(dir, "ethereum-anchors");
310
+ await mkdir(anchors);
311
+ await writeFile(join(anchors, "anchor-floor.json"), JSON.stringify(floor.anchor, null, 2));
312
+ await writeFile(join(anchors, "anchor-floor.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...floor.witness }, null, 2));
313
+ if (payload.ceiling.status === "present") {
314
+ await writeFile(join(anchors, "anchor-ceiling.json"), JSON.stringify(payload.ceiling.anchor, null, 2));
315
+ await writeFile(join(anchors, "anchor-ceiling.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...payload.ceiling.witness }, null, 2));
316
+ }
317
+ }
318
+
244
319
  async function main(): Promise<number> {
245
320
  let parsed: ParsedArgs | "help";
246
321
  try {
@@ -271,8 +346,8 @@ async function main(): Promise<number> {
271
346
  }
272
347
 
273
348
  /* A single carrier file: unpack it to a temp bundle (the committed bytes,
274
- the proof, the floor anchor and witness, the ceiling pair when present)
275
- and audit THAT, after printing the carrier's own offline verdict. The
349
+ the proof, and the floor: for /1 and /2 the floor anchor and witness and
350
+ the ceiling pair when present; for /3 the Base floor header) and audit THAT, after printing the carrier's own offline verdict. The
276
351
  temp dir is the audit's input, so the reports describe exactly what the
277
352
  file carries; everything else about the run is unchanged. Detection is
278
353
  from the last 8 bytes only, so no ordinary bundle path changes behaviour. */
@@ -301,11 +376,17 @@ async function main(): Promise<number> {
301
376
  ((verdict.payload.proof as { environment?: { attestation?: { reportB64?: string } } }).environment?.attestation?.reportB64) ?? ""
302
377
  )
303
378
  : null;
379
+ // carrier/3 (enclave v10): the floor is a Base block and no closing
380
+ // anchor exists; order after the record is the chain of proof hashes.
381
+ const v3 = verdict.payload?.carrier === CARRIER_VERSION_3 || b?.notBefore.chain === "base";
382
+ const chainWord = b?.notBefore.chain === "base" ? "Base" : "Ethereum";
304
383
  const lines = [
305
384
  `carrier: ${verdict.verdict}${verdict.carrier === "corrupt" ? " (block unreadable: corrupted, not judged)" : ""}`,
306
- b ? ` no earlier than: block ${b.notBefore.blockNumber}${b.notBefore.timestamp !== null ? ` (mined ${new Date(b.notBefore.timestamp * 1000).toISOString()})` : ""}` : null,
385
+ b ? ` no earlier than: ${chainWord} block ${b.notBefore.blockNumber}${b.notBefore.timestamp !== null ? ` (${v3 ? "stamped" : "mined"} ${new Date(b.notBefore.timestamp * 1000).toISOString()})` : ""}` : null,
307
386
  attestedMs !== null ? ` committed: ${new Date(attestedMs).toISOString()} per the enclave platform's signed clock` : null,
308
- b ? ` committed before: ${b.notAfter === null ? "NOT FETCHED (the closing anchor is not inside this file)" : `the anchoring of block ${b.notAfter.blockNumber}${b.notAfter.timestamp !== null ? ` (that block mined ${new Date(b.notAfter.timestamp * 1000).toISOString()})` : ""}`}` : null,
387
+ b && v3
388
+ ? " committed before: no ceiling in position (none exists for a Base floor: order after the record is the chain of proof hashes)"
389
+ : b ? ` committed before: ${b.notAfter === null ? "NOT FETCHED (the closing anchor is not inside this file)" : `the anchoring of block ${b.notAfter.blockNumber}${b.notAfter.timestamp !== null ? ` (that block mined ${new Date(b.notAfter.timestamp * 1000).toISOString()})` : ""}`}` : null,
309
390
  ...verdict.reasons.map((r) => ` - ${r}`),
310
391
  ].filter((l): l is string => l !== null);
311
392
  process.stdout.write(lines.join("\n") + "\n");
@@ -316,14 +397,7 @@ async function main(): Promise<number> {
316
397
  const name = basename(parsed.bundlePath).replace(/\.bitgraph(\.[^.]+)$/i, "$1").replace(/\.bitgraph$/i, "") || "artifact";
317
398
  await writeFile(join(dir, name), parsedCarrier.inner);
318
399
  await writeFile(join(dir, "proof.json"), JSON.stringify(parsedCarrier.payload.proof, null, 2));
319
- const anchors = join(dir, "ethereum-anchors");
320
- await mkdir(anchors);
321
- await writeFile(join(anchors, "anchor-floor.json"), JSON.stringify(parsedCarrier.payload.floor.anchor, null, 2));
322
- await writeFile(join(anchors, "anchor-floor.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...parsedCarrier.payload.floor.witness }, null, 2));
323
- if (parsedCarrier.payload.ceiling.status === "present") {
324
- await writeFile(join(anchors, "anchor-ceiling.json"), JSON.stringify(parsedCarrier.payload.ceiling.anchor, null, 2));
325
- await writeFile(join(anchors, "anchor-ceiling.witness.json"), JSON.stringify({ version: "bitgraph-anchor-witness/1", ...parsedCarrier.payload.ceiling.witness }, null, 2));
326
- }
400
+ await writeCarrierFloor(dir, parsedCarrier.payload);
327
401
  bundlePath = dir;
328
402
  }
329
403
  }
@@ -378,6 +452,27 @@ async function main(): Promise<number> {
378
452
  for (const line of c.settlement?.lines ?? []) process.stdout.write(` ${line}\n`);
379
453
  }
380
454
  for (const s of result.ceilings?.statuses ?? []) process.stdout.write(`ceiling ${s.status} ${s.path}: ${s.note}\n`);
455
+ for (const p of result.temporal.floorProblems ?? []) process.stdout.write(`floor PROBLEM ${p.code}${p.path !== undefined ? ` ${p.path}` : ""}: ${p.message}\n`);
456
+ for (const e of result.exports?.checks ?? []) {
457
+ if (e.status !== "checked") {
458
+ process.stdout.write(`export NOT CHECKED ${e.path}: ${e.reason ?? e.status}\n`);
459
+ continue;
460
+ }
461
+ const withFile = e.runs.filter((r) => r.file !== null).length;
462
+ const files = e.files.length === 0 ? "no covered file in the bundle" : `${withFile} of ${e.files.length} covered file${e.files.length === 1 ? "" : "s"} checked`;
463
+ process.stdout.write(`export ${e.verdict} ${e.path}: ${e.kind ?? "export"}, ${files}${e.failedClaims.length > 0 ? `; FALSE: ${e.failedClaims.join(", ")}` : ""}\n`);
464
+ // The three time claims, one line each, never merged.
465
+ const t = e.times;
466
+ const when = (unix: number) => new Date(unix * 1000).toISOString();
467
+ const firstClaims = e.runs[0] !== undefined ? exportRunClaims(e, e.runs[0]) : e.claims;
468
+ const why = (id: string) => {
469
+ const c = firstClaims.find((x) => x.id === id);
470
+ return c ? `not established (${c.result}: ${c.detail})` : "not established";
471
+ };
472
+ process.stdout.write(` floor: ${t.floor ? `committed bytes finished after ${t.floor.chain === "base" ? "Base" : "Ethereum"} block ${t.floor.blockNumber} (${t.floor.chain === "base" ? "stamped" : "mined"} ${when(t.floor.blockTimestamp)})` : why("floor.header")}\n`);
473
+ process.stdout.write(` ceiling on Base: ${t.ceilingBase ? `existed by Base block ${t.ceilingBase.blockNumber} (${when(t.ceilingBase.blockTimestamp)})${t.ceilingBase.provisional ? ", provisional until checked against Base" : ""}` : why("ceiling.base")}\n`);
474
+ process.stdout.write(` ceiling on Ethereum: ${t.ceilingEthereum ? `existed by Ethereum block ${t.ceilingEthereum.blockNumber} (${when(t.ceilingEthereum.blockTimestamp)})` : why("ceiling.ethereum")}\n`);
475
+ }
381
476
  process.stdout.write(
382
477
  `bitgraph-audit ${auditToolVersion()}: wrote ${written.join(", ")}\n` +
383
478
  `exit ${flags.code}: ${exitMeaning(flags)}\n`