@mikeargento/bitgraph-audit 0.6.3 → 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/anomalies.js +10 -10
- package/dist/anomalies.js.map +1 -1
- 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 +35 -14
- 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 +3 -3
- package/src/anomalies.ts +10 -10
- 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 +34 -14
- package/src/types.ts +54 -0
package/src/report-md.ts
CHANGED
|
@@ -176,10 +176,10 @@ function executiveSummary(
|
|
|
176
176
|
lines.push(
|
|
177
177
|
"Proofs are grouped by the authority key, epoch, and chain they were " +
|
|
178
178
|
"committed under; each group is called a partition. Every proof " +
|
|
179
|
-
"occupies two counter positions: one reserved in advance
|
|
180
|
-
"and one at the moment of commit.
|
|
179
|
+
"occupies two counter positions: one reserved in advance " +
|
|
180
|
+
"and one at the moment of commit. Reserved positions never produce " +
|
|
181
181
|
"stored proof files of their own, so a healthy chain shows only " +
|
|
182
|
-
"committed proofs, each referencing its own
|
|
182
|
+
"committed proofs, each referencing its own reserved position from " +
|
|
183
183
|
"inside its signed content."
|
|
184
184
|
);
|
|
185
185
|
lines.push("");
|
|
@@ -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("");
|
|
@@ -412,10 +432,10 @@ function partitionSummarySentences(report: AuditJsonReport, partition: ReportPar
|
|
|
412
432
|
const count = detail.count;
|
|
413
433
|
const singular = count === "1";
|
|
414
434
|
out.push(
|
|
415
|
-
`${withCommas(count)} counter ${singular ? "position is neither a commit position nor a referenced
|
|
435
|
+
`${withCommas(count)} counter ${singular ? "position is neither a commit position nor a referenced reserved position" : "positions are neither commit positions nor referenced reserved positions"} ` +
|
|
416
436
|
"in the supplied bundle. An unexplained interior position may " +
|
|
417
|
-
"mean a proof is absent from the bundle, or a
|
|
418
|
-
"
|
|
437
|
+
"mean a proof is absent from the bundle, or a position that was " +
|
|
438
|
+
"reserved but never committed (a routine, benign occurrence); " +
|
|
419
439
|
"this offline audit cannot tell the two apart. It does not, by " +
|
|
420
440
|
"itself, prove that the BitGraph authority failed to create or " +
|
|
421
441
|
"withheld any proof."
|
|
@@ -449,7 +469,7 @@ function divergenceSummarySentence(divergence: DivergenceRecord): string {
|
|
|
449
469
|
);
|
|
450
470
|
case "slot-collision":
|
|
451
471
|
return (
|
|
452
|
-
`${head} ${n === 2 ? "both reference" : "all reference"}
|
|
472
|
+
`${head} ${n === 2 ? "both reference" : "all reference"} reserved position ` +
|
|
453
473
|
`${withCommas(divergence.contested["slotCounter"] ?? "?")} in the same epoch.` +
|
|
454
474
|
tail
|
|
455
475
|
);
|
|
@@ -457,7 +477,7 @@ function divergenceSummarySentence(divergence: DivergenceRecord): string {
|
|
|
457
477
|
return (
|
|
458
478
|
`${head} allocate one causal position ` +
|
|
459
479
|
`${withCommas(divergence.contested["position"] ?? "?")} in the same epoch, at least one ` +
|
|
460
|
-
"committing it and at least one reserving it
|
|
480
|
+
"committing it and at least one reserving it." +
|
|
461
481
|
tail
|
|
462
482
|
);
|
|
463
483
|
case "predecessor-reuse":
|
|
@@ -558,7 +578,7 @@ function perPartitionChains(
|
|
|
558
578
|
proof?.verificationStatus ?? "unverified",
|
|
559
579
|
]);
|
|
560
580
|
}
|
|
561
|
-
lines.push(...table(["Counter", "
|
|
581
|
+
lines.push(...table(["Counter", "Reserved", "Proof hash", "Predecessor", "Status"], rows));
|
|
562
582
|
if (component.memberProofHashes.length > MAX_TABLE_ROWS) {
|
|
563
583
|
lines.push("");
|
|
564
584
|
lines.push(
|
|
@@ -675,7 +695,7 @@ function divergenceDetails(lines: string[], report: AuditJsonReport): void {
|
|
|
675
695
|
|
|
676
696
|
function partyTable(parties: DivergenceRecord["parties"]): string[] {
|
|
677
697
|
return table(
|
|
678
|
-
["Proof hash", "Counter", "
|
|
698
|
+
["Proof hash", "Counter", "Reserved", "Predecessor", "Status", "Sources"],
|
|
679
699
|
parties.map((party) => [
|
|
680
700
|
inlineCode(party.proofHash),
|
|
681
701
|
party.counter ?? "",
|
|
@@ -963,13 +983,13 @@ function codeMeaning(code: AnomalyCode): string {
|
|
|
963
983
|
case "manifest-contents-hash-mismatch":
|
|
964
984
|
return "The manifest's declared contents hash does not match the computed one. The manifest is advisory; the computed value governs.";
|
|
965
985
|
case "unexplained-counter-positions":
|
|
966
|
-
return "Counter positions inside the observed range are neither commit positions nor referenced
|
|
986
|
+
return "Counter positions inside the observed range are neither commit positions nor referenced reserved positions. Such a position may be a proof absent from the bundle, or a position that was reserved but never committed (routine and benign); this offline audit cannot distinguish them, and it does not, by itself, prove the authority failed to create or withheld any proof.";
|
|
967
987
|
case "counter-collision":
|
|
968
988
|
return "Two or more valid proofs claim the same commit counter. All are preserved; the audit tool does not choose between them.";
|
|
969
989
|
case "slot-collision":
|
|
970
|
-
return "Two or more valid proofs reference the same
|
|
990
|
+
return "Two or more valid proofs reference the same reserved position. All are preserved; the audit tool does not choose between them.";
|
|
971
991
|
case "cross-kind-position-reuse":
|
|
972
|
-
return "A commit counter in one proof equals a different valid proof's
|
|
992
|
+
return "A commit counter in one proof equals a different valid proof's reserved position in the same partition: one causal position allocated twice across kinds, which only enclave malfunction, replay, or compromise produces. All parties are preserved; the audit tool does not choose between them.";
|
|
973
993
|
case "predecessor-reuse":
|
|
974
994
|
return "Two or more valid proofs name the same predecessor: the chain forks at that point. All branches are preserved; the audit tool does not choose between them.";
|
|
975
995
|
case "chain-break-missing":
|
|
@@ -981,7 +1001,7 @@ function codeMeaning(code: AnomalyCode): string {
|
|
|
981
1001
|
case "multiple-genesis":
|
|
982
1002
|
return "More than one proof in the same partition starts a chain without naming a predecessor. A single such proof is the normal start of an epoch; several need adjudication.";
|
|
983
1003
|
case "slot-order-violation":
|
|
984
|
-
return "A proof's
|
|
1004
|
+
return "A proof's reserved position is not strictly before its commit position, violating the reserve-then-commit ordering.";
|
|
985
1005
|
case "epochlink-terminal-missing":
|
|
986
1006
|
return "An epoch's first proof links to a prior epoch that is observed, but the specific proof it names is absent from the bundle.";
|
|
987
1007
|
case "epochlink-dangling":
|
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"];
|