@mikeargento/bitgraph-audit 0.6.4 → 0.7.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/src/cli.ts CHANGED
@@ -52,7 +52,7 @@ const KNOWN_POLICY_FIELDS: readonly string[] = [
52
52
  class UsageError extends Error {}
53
53
 
54
54
  const USAGE_LINE =
55
- "Usage: bitgraph-audit <path-to-bundle> [--out <dir>] [--format json,md] [--trust-policy <path>]";
55
+ "Usage: bitgraph-audit <path-to-bundle> [--out <dir>] [--format json,md] [--trust-policy <path>] [--ceiling-writer <0x...>] [--ceiling-chain <id>]";
56
56
 
57
57
  function helpText(): string {
58
58
  return [
@@ -78,6 +78,14 @@ function helpText(): string {
78
78
  " verification tiers. Valid fields:",
79
79
  ` ${KNOWN_POLICY_FIELDS.join(", ")}.`,
80
80
  " Any other field is an error.",
81
+ " --ceiling-writer <0x…> The address ceilings in time must come from",
82
+ " (default: BitGraph's published writer,",
83
+ " 0xf3972408D853c975F86351C311f4310220bbF2a3).",
84
+ " --ceiling-chain <id> Base chain id for ceilings (default 8453).",
85
+ " The audit never goes online, so whether each",
86
+ " ceiling's Base block is on Base's chain is",
87
+ " reported as unchecked: confirm it on basescan.org",
88
+ " or with `bitgraph ceiling verify --rpc <url>`.",
81
89
  " --help, -h Print this help and exit 0.",
82
90
  "",
83
91
  "Exit codes (bit flags):",
@@ -98,7 +106,10 @@ function helpText(): string {
98
106
  " or a supplied anchor witness that fails its offline verification",
99
107
  " (block-hash mismatch, digest binding, block number, header or",
100
108
  " RLP malformation, an invalid candidate anchor, or an unmatched",
101
- " witness). Benign findings are reported but never set exit bits:",
109
+ " witness), or a ceiling in time (bitgraph-ceiling/1) that fails",
110
+ " its check against its proof (wrong writer, root, block, header).",
111
+ " A pending ceiling or a ceiling status note never sets a bit.",
112
+ " Benign findings are reported but never set exit bits:",
102
113
  " duplicate copies, manifest advisories, unsafe paths, embedded",
103
114
  " proofHash mismatches, and informational anchor findings (unsigned",
104
115
  " metadata disagreements, metadata-only anchor claims, and",
@@ -120,6 +131,8 @@ interface ParsedArgs {
120
131
  outDir: string;
121
132
  formats: ReadonlySet<"json" | "md">;
122
133
  trustPolicyPath?: string;
134
+ ceilingWriter?: string;
135
+ ceilingChain?: number;
123
136
  }
124
137
 
125
138
  function parseArgs(argv: string[]): ParsedArgs | "help" {
@@ -127,6 +140,8 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
127
140
  let outDir = ".";
128
141
  let formats: Set<"json" | "md"> = new Set(["json", "md"]);
129
142
  let trustPolicyPath: string | undefined;
143
+ let ceilingWriter: string | undefined;
144
+ let ceilingChain: number | undefined;
130
145
 
131
146
  const takeValue = (flag: string, index: number): string => {
132
147
  const value = argv[index + 1];
@@ -158,6 +173,15 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
158
173
  } else if (arg === "--trust-policy") {
159
174
  trustPolicyPath = takeValue(arg, i);
160
175
  i++;
176
+ } else if (arg === "--ceiling-writer") {
177
+ ceilingWriter = takeValue(arg, i);
178
+ i++;
179
+ if (!/^0x[0-9a-fA-F]{40}$/.test(ceilingWriter)) throw new UsageError("--ceiling-writer must be a 0x address.");
180
+ } else if (arg === "--ceiling-chain") {
181
+ ceilingChain = Number(takeValue(arg, i));
182
+ i++;
183
+ if (!Number.isInteger(ceilingChain) || ceilingChain <= 0) throw new UsageError("--ceiling-chain must be a chain id.");
184
+
161
185
  } else if (arg.startsWith("-")) {
162
186
  throw new UsageError(`unknown option "${arg}".`);
163
187
  } else if (bundlePath === undefined) {
@@ -173,6 +197,9 @@ function parseArgs(argv: string[]): ParsedArgs | "help" {
173
197
  outDir,
174
198
  formats,
175
199
  ...(trustPolicyPath !== undefined ? { trustPolicyPath } : {}),
200
+ ...(ceilingWriter !== undefined ? { ceilingWriter } : {}),
201
+ ...(ceilingChain !== undefined ? { ceilingChain } : {}),
202
+
176
203
  };
177
204
  }
178
205
 
@@ -209,7 +236,7 @@ function exitMeaning(flags: ExitFlags): string {
209
236
  const parts: string[] = [];
210
237
  if (flags.verificationFailures) parts.push("verification failures");
211
238
  if (flags.chainAnomaliesOrDivergences) {
212
- parts.push("chain anomalies, divergences, or anchor witness verification failures");
239
+ parts.push("chain anomalies, divergences, or anchor witness verification failures (or a failed ceiling in time)");
213
240
  }
214
241
  return parts.join("; ");
215
242
  }
@@ -308,10 +335,15 @@ async function main(): Promise<number> {
308
335
 
309
336
  let result;
310
337
  try {
311
- result = await runAudit(
312
- bundlePath,
313
- trustAnchors !== undefined ? { trustAnchors } : undefined
314
- );
338
+ const ceilings = {
339
+ ...(parsed.ceilingWriter !== undefined ? { writer: parsed.ceilingWriter } : {}),
340
+ ...(parsed.ceilingChain !== undefined ? { chainId: parsed.ceilingChain } : {}),
341
+
342
+ };
343
+ result = await runAudit(bundlePath, {
344
+ ...(trustAnchors !== undefined ? { trustAnchors } : {}),
345
+ ceilings,
346
+ });
315
347
  } catch (err) {
316
348
  process.stderr.write(
317
349
  `bitgraph-audit: cannot audit "${parsed.bundlePath}": ` +
@@ -338,6 +370,13 @@ async function main(): Promise<number> {
338
370
  if (carrierFlags & 1) { flags.verificationFailures = true; }
339
371
  if (carrierFlags & 2) { flags.chainAnomaliesOrDivergences = true; }
340
372
  flags.code = flags.code | carrierFlags;
373
+ for (const c of result.ceilings?.checks ?? []) {
374
+ const width = c.window?.widthSeconds != null ? ` (window ${c.window.widthSeconds}s after the floor block)` : "";
375
+ process.stdout.write(c.status === "verified"
376
+ ? `ceiling VERIFIED ${c.path}: ${c.label ?? ""}${width}; ${c.onChainDetail ?? ""}\n`
377
+ : `ceiling ${c.status.toUpperCase()} ${c.path}: ${c.reason ?? ""}\n`);
378
+ }
379
+ for (const s of result.ceilings?.statuses ?? []) process.stdout.write(`ceiling ${s.status} ${s.path}: ${s.note}\n`);
341
380
  process.stdout.write(
342
381
  `bitgraph-audit ${auditToolVersion()}: wrote ${written.join(", ")}\n` +
343
382
  `exit ${flags.code}: ${exitMeaning(flags)}\n`
package/src/index.ts CHANGED
@@ -101,6 +101,8 @@ export { identifyAnchors } from "./anchors.js";
101
101
  export { verifyAnchorWitnesses, verifyAnchorWitness } from "./witness.js";
102
102
 
103
103
  export { deriveTemporalBounds } from "./temporal.js";
104
+ export { verifyCeilings, BITGRAPH_CEILING_WRITER, BASE_MAINNET_CHAIN_ID } from "./ceilings.js";
105
+ export type { CeilingAuditOptions } from "./ceilings.js";
104
106
 
105
107
  export { validateAttestations, validateNitroAttestationDocument } from "./attestation.js";
106
108
 
package/src/ingest.ts CHANGED
@@ -43,6 +43,7 @@ import { readTarEntries } from "./tar.js";
43
43
  import { combineEntryDigests } from "./contents-hash.js";
44
44
  import type {
45
45
  AnchorWitnessFile,
46
+ CeilingFile,
46
47
  ArtifactRecord,
47
48
  AuditFinding,
48
49
  BundleManifest,
@@ -82,6 +83,8 @@ const NUL = new Uint8Array([0]);
82
83
  const MANIFEST_PATH = "manifest.json";
83
84
  const BUNDLE_VERSION = "bitgraph-bundle/1";
84
85
  const WITNESS_VERSION = "bitgraph-anchor-witness/1";
86
+ const CEILING_VERSION_TAG = "bitgraph-ceiling/1";
87
+ const CEILING_STATUS_VERSION_TAG = "bitgraph-ceiling-status/1";
85
88
 
86
89
  // ---------------------------------------------------------------------------
87
90
  // Public API
@@ -336,6 +339,8 @@ function finalizeIngest(params: FinalizeParams): IngestResult {
336
339
  const proofsByHash = new Map<string, ObservedProof>();
337
340
  const unsupportedVersions: UnsupportedVersionRecord[] = [];
338
341
  const witnesses: AnchorWitnessFile[] = [];
342
+ const ceilings: CeilingFile[] = [];
343
+ const ceilingStatuses: CeilingFile[] = [];
339
344
  const artifactsByHex = new Map<string, ArtifactRecord>();
340
345
  let manifest: ManifestReport | undefined;
341
346
  let proofFiles = 0;
@@ -393,6 +398,17 @@ function finalizeIngest(params: FinalizeParams): IngestResult {
393
398
  continue;
394
399
  }
395
400
 
401
+ // A ceiling in time travels beside its proof: evidence, never an artifact.
402
+ const pv = parsed !== undefined ? (parsed as Record<string, unknown>)["version"] : undefined;
403
+ if (pv === CEILING_VERSION_TAG || pv === CEILING_STATUS_VERSION_TAG) {
404
+ (pv === CEILING_VERSION_TAG ? ceilings : ceilingStatuses).push({
405
+ path: entry.path,
406
+ fileSha256Hex: entry.sha256Hex,
407
+ json: parsed as Record<string, unknown>,
408
+ });
409
+ continue;
410
+ }
411
+
396
412
  indexArtifact(entry, artifactsByHex);
397
413
  }
398
414
 
@@ -420,6 +436,7 @@ function finalizeIngest(params: FinalizeParams): IngestResult {
420
436
  unsupportedVersion: unsupportedVersions.length,
421
437
  artifacts: artifacts.length,
422
438
  witnesses: witnesses.length,
439
+ ceilings: ceilings.length + ceilingStatuses.length,
423
440
  skippedUnsafePaths,
424
441
  };
425
442
 
@@ -432,6 +449,8 @@ function finalizeIngest(params: FinalizeParams): IngestResult {
432
449
  unsupportedVersions,
433
450
  artifacts,
434
451
  witnesses,
452
+ ceilings,
453
+ ceilingStatuses,
435
454
  ...(manifest !== undefined ? { manifest } : {}),
436
455
  computedContentsHashB64,
437
456
  findings,
@@ -109,6 +109,9 @@ export function buildJsonReport(result: AuditResult): AuditJsonReport {
109
109
  groups: result.authorities.groups,
110
110
  sharedSignersAcrossEpochs: result.authorities.sharedSignersAcrossEpochs,
111
111
  },
112
+ ...(result.ceilings && (result.ceilings.checks.length > 0 || result.ceilings.statuses.length > 0)
113
+ ? { ceilings: result.ceilings }
114
+ : {}),
112
115
  attestations: {
113
116
  records: result.attestations.records,
114
117
  counts: result.attestations.counts,
package/src/report-md.ts CHANGED
@@ -333,6 +333,26 @@ function executiveSummary(
333
333
  );
334
334
  lines.push("");
335
335
 
336
+ // Ceilings in time (bitgraph-ceiling/1), only when the bundle carries any.
337
+ const cl = report.ceilings;
338
+ if (cl && (cl.checks.length > 0 || cl.statuses.length > 0)) {
339
+ lines.push("### Ceilings in time (Base)");
340
+ lines.push("");
341
+ lines.push(`Checked against writer ${inlineCode(cl.writer)} on chain ${cl.chainId}.`);
342
+ lines.push("");
343
+ for (const c of cl.checks) {
344
+ if (c.status === "verified") {
345
+ const w = c.window!;
346
+ const width = w.widthSeconds != null ? `, ${w.widthSeconds} s after the floor block` : "";
347
+ lines.push(`- VERIFIED ${inlineCode(c.path)}: ${c.label ?? ""}${width}. ${c.onChainDetail ?? ""}.`);
348
+ } else {
349
+ lines.push(`- ${c.status.toUpperCase()} ${inlineCode(c.path)}: ${c.reason ?? ""}`);
350
+ }
351
+ }
352
+ for (const st of cl.statuses) lines.push(`- ${st.status} ${inlineCode(st.path)}: ${st.note}`);
353
+ lines.push("");
354
+ }
355
+
336
356
  // External time evidence.
337
357
  lines.push("### External time evidence");
338
358
  lines.push("");
package/src/types.ts CHANGED
@@ -406,6 +406,8 @@ export interface IngestCounts {
406
406
  artifacts: number;
407
407
  /** Anchor witness files. */
408
408
  witnesses: number;
409
+ /** Ceiling files (bitgraph-ceiling/1) and ceiling status notes (bitgraph-ceiling-status/1). */
410
+ ceilings?: number;
409
411
  /** Container entries skipped for unsafe paths. */
410
412
  skippedUnsafePaths: number;
411
413
  }
@@ -467,6 +469,10 @@ export interface IngestResult {
467
469
  artifacts: ArtifactRecord[];
468
470
  /** Anchor witness files in observation order. */
469
471
  witnesses: AnchorWitnessFile[];
472
+ /** Ceiling files (bitgraph-ceiling/1) in observation order. Optional so older embedders' IngestResults still type. */
473
+ ceilings?: CeilingFile[];
474
+ /** Ceiling status notes (bitgraph-ceiling-status/1): a package saying why a ceiling is absent. */
475
+ ceilingStatuses?: CeilingFile[];
470
476
  /** Present when a root manifest.json entry existed. */
471
477
  manifest?: ManifestReport;
472
478
 
@@ -1306,6 +1312,8 @@ export interface AuditOptions {
1306
1312
  * explicitly non-AWS deployments.
1307
1313
  */
1308
1314
  trustedRootCaDer?: Uint8Array;
1315
+ /** Ceilings in time: the declared writer, the chain, and an optional Base RPC for the one online check. */
1316
+ ceilings?: import("./ceilings.js").CeilingAuditOptions;
1309
1317
  }
1310
1318
 
1311
1319
  /**
@@ -1325,6 +1333,48 @@ export interface AuditRunMetadata {
1325
1333
  }
1326
1334
 
1327
1335
  /** Everything one full audit run produced, in pipeline order. */
1336
+ /** A ceiling file or a ceiling status note, as found in the bundle. Never an artifact. */
1337
+ export interface CeilingFile {
1338
+ path: string;
1339
+ fileSha256Hex: string;
1340
+ json: Record<string, unknown>;
1341
+ }
1342
+
1343
+ /** One ceiling file checked against its proof (verifyCeiling), offline. */
1344
+ export interface CeilingCheck {
1345
+ path: string;
1346
+ proofHash: string | null;
1347
+ /**
1348
+ * verified: every offline check passed. failed: the file is present and
1349
+ * wrong (a tampered or foreign ceiling). pending: the file says no Base
1350
+ * transaction yet. unmatched: no proof in the bundle has its proofHash.
1351
+ */
1352
+ status: "verified" | "failed" | "pending" | "unmatched";
1353
+ reason?: string;
1354
+ /** One line for people, e.g. "Ceiling: Base block N at hh:mm:ss UTC, settled on Ethereum." */
1355
+ label?: string;
1356
+ window?: {
1357
+ floorBlock: number | null;
1358
+ floorTime: number | null;
1359
+ ceilingChainId: number;
1360
+ ceilingBlock: number;
1361
+ ceilingTime: number;
1362
+ widthSeconds: number | null;
1363
+ };
1364
+ /** Online check against a Base RPC: null when not asked or unreachable. */
1365
+ onChain: boolean | null;
1366
+ onChainDetail?: string;
1367
+ }
1368
+
1369
+ export interface CeilingAnalysis {
1370
+ /** The writer address the ceilings were checked against. */
1371
+ writer: string;
1372
+ chainId: number;
1373
+ checks: CeilingCheck[];
1374
+ /** Status notes carried instead of a ceiling (path and note). */
1375
+ statuses: Array<{ path: string; status: string; note: string }>;
1376
+ }
1377
+
1328
1378
  export interface AuditResult {
1329
1379
  runMetadata: AuditRunMetadata;
1330
1380
  ingest: IngestResult;
@@ -1336,6 +1386,8 @@ export interface AuditResult {
1336
1386
  witnesses: AnchorWitnessAnalysis;
1337
1387
  temporal: TemporalAnalysis;
1338
1388
  attestations: AttestationAnalysis;
1389
+ /** Ceilings in time on Base. Absent from results made before this stage existed. */
1390
+ ceilings?: CeilingAnalysis;
1339
1391
  }
1340
1392
 
1341
1393
  /**
@@ -1577,6 +1629,8 @@ export interface AuditJsonReport {
1577
1629
  groups: AuthorityGroup[];
1578
1630
  sharedSignersAcrossEpochs: SignerEpochSpan[];
1579
1631
  };
1632
+ /** Ceilings in time on Base (bitgraph-ceiling/1). Absent when the bundle carries none. */
1633
+ ceilings?: import("./types.js").CeilingAnalysis;
1580
1634
  attestations: {
1581
1635
  records: ProofAttestationRecord[];
1582
1636
  counts: AttestationAnalysis["counts"];