@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/dist/audit.d.ts +1 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +9 -2
- package/dist/audit.js.map +1 -1
- package/dist/ceilings.d.ts +16 -0
- package/dist/ceilings.d.ts.map +1 -0
- package/dist/ceilings.js +70 -0
- package/dist/ceilings.js.map +1 -0
- package/dist/cli.js +46 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +17 -0
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +3 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.js +21 -0
- package/dist/report-md.js.map +1 -1
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/audit.ts +8 -1
- package/src/ceilings.ts +85 -0
- package/src/cli.ts +46 -7
- package/src/index.ts +2 -0
- package/src/ingest.ts +19 -0
- package/src/report-json.ts +3 -0
- package/src/report-md.ts +20 -0
- package/src/types.ts +54 -0
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)
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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,
|
package/src/report-json.ts
CHANGED
|
@@ -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"];
|