@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.
- package/README.md +8 -2
- package/dist/audit.d.ts +9 -6
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +34 -8
- package/dist/audit.js.map +1 -1
- package/dist/ceilings.d.ts.map +1 -1
- package/dist/ceilings.js +1 -0
- package/dist/ceilings.js.map +1 -1
- package/dist/cli.js +109 -21
- 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 +374 -0
- package/dist/exports.js.map +1 -0
- package/dist/floors.d.ts +23 -0
- package/dist/floors.d.ts.map +1 -0
- package/dist/floors.js +203 -0
- package/dist/floors.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +2 -0
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +193 -19
- package/dist/ingest.js.map +1 -1
- package/dist/report-json.d.ts.map +1 -1
- package/dist/report-json.js +30 -0
- package/dist/report-json.js.map +1 -1
- package/dist/report-md.d.ts.map +1 -1
- package/dist/report-md.js +315 -25
- package/dist/report-md.js.map +1 -1
- package/dist/temporal.d.ts +12 -0
- package/dist/temporal.d.ts.map +1 -1
- package/dist/temporal.js +245 -41
- package/dist/temporal.js.map +1 -1
- package/dist/types.d.ts +348 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/base-floor.test.ts +405 -0
- package/src/audit.ts +36 -8
- package/src/ceilings.ts +1 -0
- package/src/cli.ts +117 -22
- package/src/exports.ts +482 -0
- package/src/floors.ts +242 -0
- package/src/index.ts +20 -0
- package/src/ingest.ts +207 -18
- package/src/report-json.ts +30 -0
- package/src/report-md.ts +353 -29
- package/src/temporal.ts +279 -48
- 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
|
|
65
|
-
"a carrier is unpacked and audited as the
|
|
66
|
-
"own verdict (TRUE / FALSE / UNDETERMINED,
|
|
67
|
-
"printed first.",
|
|
68
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|