@mikeargento/bitgraph 1.1.0 → 1.2.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 (195) hide show
  1. package/README.md +49 -40
  2. package/dist/__tests__/audit-anchors.test.d.ts +2 -0
  3. package/dist/__tests__/audit-anchors.test.d.ts.map +1 -0
  4. package/dist/__tests__/audit-anchors.test.js +152 -0
  5. package/dist/__tests__/audit-anchors.test.js.map +1 -0
  6. package/dist/__tests__/audit-anomalies.test.d.ts +2 -0
  7. package/dist/__tests__/audit-anomalies.test.d.ts.map +1 -0
  8. package/dist/__tests__/audit-anomalies.test.js +663 -0
  9. package/dist/__tests__/audit-anomalies.test.js.map +1 -0
  10. package/dist/__tests__/audit-attestation.test.d.ts +2 -0
  11. package/dist/__tests__/audit-attestation.test.d.ts.map +1 -0
  12. package/dist/__tests__/audit-attestation.test.js +403 -0
  13. package/dist/__tests__/audit-attestation.test.js.map +1 -0
  14. package/dist/__tests__/audit-bench-smoke.test.d.ts +2 -0
  15. package/dist/__tests__/audit-bench-smoke.test.d.ts.map +1 -0
  16. package/dist/__tests__/audit-bench-smoke.test.js +87 -0
  17. package/dist/__tests__/audit-bench-smoke.test.js.map +1 -0
  18. package/dist/__tests__/audit-boundary.test.d.ts +20 -0
  19. package/dist/__tests__/audit-boundary.test.d.ts.map +1 -0
  20. package/dist/__tests__/audit-boundary.test.js +84 -0
  21. package/dist/__tests__/audit-boundary.test.js.map +1 -0
  22. package/dist/__tests__/audit-cli.test.d.ts +2 -0
  23. package/dist/__tests__/audit-cli.test.d.ts.map +1 -0
  24. package/dist/__tests__/audit-cli.test.js +239 -0
  25. package/dist/__tests__/audit-cli.test.js.map +1 -0
  26. package/dist/__tests__/audit-dependencies.test.d.ts +2 -0
  27. package/dist/__tests__/audit-dependencies.test.d.ts.map +1 -0
  28. package/dist/__tests__/audit-dependencies.test.js +116 -0
  29. package/dist/__tests__/audit-dependencies.test.js.map +1 -0
  30. package/dist/__tests__/audit-fixtures.d.ts +265 -0
  31. package/dist/__tests__/audit-fixtures.d.ts.map +1 -0
  32. package/dist/__tests__/audit-fixtures.js +511 -0
  33. package/dist/__tests__/audit-fixtures.js.map +1 -0
  34. package/dist/__tests__/audit-ingest.test.d.ts +2 -0
  35. package/dist/__tests__/audit-ingest.test.d.ts.map +1 -0
  36. package/dist/__tests__/audit-ingest.test.js +473 -0
  37. package/dist/__tests__/audit-ingest.test.js.map +1 -0
  38. package/dist/__tests__/audit-memory-ingest.test.d.ts +12 -0
  39. package/dist/__tests__/audit-memory-ingest.test.d.ts.map +1 -0
  40. package/dist/__tests__/audit-memory-ingest.test.js +120 -0
  41. package/dist/__tests__/audit-memory-ingest.test.js.map +1 -0
  42. package/dist/__tests__/audit-property.test.d.ts +2 -0
  43. package/dist/__tests__/audit-property.test.d.ts.map +1 -0
  44. package/dist/__tests__/audit-property.test.js +771 -0
  45. package/dist/__tests__/audit-property.test.js.map +1 -0
  46. package/dist/__tests__/audit-real-fixtures.test.d.ts +2 -0
  47. package/dist/__tests__/audit-real-fixtures.test.d.ts.map +1 -0
  48. package/dist/__tests__/audit-real-fixtures.test.js +132 -0
  49. package/dist/__tests__/audit-real-fixtures.test.js.map +1 -0
  50. package/dist/__tests__/audit-real-live.test.d.ts +2 -0
  51. package/dist/__tests__/audit-real-live.test.d.ts.map +1 -0
  52. package/dist/__tests__/audit-real-live.test.js +107 -0
  53. package/dist/__tests__/audit-real-live.test.js.map +1 -0
  54. package/dist/__tests__/audit-reconstruct.test.d.ts +2 -0
  55. package/dist/__tests__/audit-reconstruct.test.d.ts.map +1 -0
  56. package/dist/__tests__/audit-reconstruct.test.js +287 -0
  57. package/dist/__tests__/audit-reconstruct.test.js.map +1 -0
  58. package/dist/__tests__/audit-report.test.d.ts +2 -0
  59. package/dist/__tests__/audit-report.test.d.ts.map +1 -0
  60. package/dist/__tests__/audit-report.test.js +186 -0
  61. package/dist/__tests__/audit-report.test.js.map +1 -0
  62. package/dist/__tests__/audit-roundtrip.test.d.ts +2 -0
  63. package/dist/__tests__/audit-roundtrip.test.d.ts.map +1 -0
  64. package/dist/__tests__/audit-roundtrip.test.js +368 -0
  65. package/dist/__tests__/audit-roundtrip.test.js.map +1 -0
  66. package/dist/__tests__/audit-temporal.test.d.ts +2 -0
  67. package/dist/__tests__/audit-temporal.test.d.ts.map +1 -0
  68. package/dist/__tests__/audit-temporal.test.js +364 -0
  69. package/dist/__tests__/audit-temporal.test.js.map +1 -0
  70. package/dist/__tests__/audit-tiers.test.d.ts +2 -0
  71. package/dist/__tests__/audit-tiers.test.d.ts.map +1 -0
  72. package/dist/__tests__/audit-tiers.test.js +282 -0
  73. package/dist/__tests__/audit-tiers.test.js.map +1 -0
  74. package/dist/__tests__/audit-witness-real.test.d.ts +19 -0
  75. package/dist/__tests__/audit-witness-real.test.d.ts.map +1 -0
  76. package/dist/__tests__/audit-witness-real.test.js +82 -0
  77. package/dist/__tests__/audit-witness-real.test.js.map +1 -0
  78. package/dist/__tests__/audit-witness.test.d.ts +2 -0
  79. package/dist/__tests__/audit-witness.test.d.ts.map +1 -0
  80. package/dist/__tests__/audit-witness.test.js +308 -0
  81. package/dist/__tests__/audit-witness.test.js.map +1 -0
  82. package/dist/__tests__/chain-hash-anchor.test.d.ts +22 -0
  83. package/dist/__tests__/chain-hash-anchor.test.d.ts.map +1 -0
  84. package/dist/__tests__/chain-hash-anchor.test.js +55 -0
  85. package/dist/__tests__/chain-hash-anchor.test.js.map +1 -0
  86. package/dist/__tests__/declared-proof-fixture.d.ts +66 -0
  87. package/dist/__tests__/declared-proof-fixture.d.ts.map +1 -0
  88. package/dist/__tests__/declared-proof-fixture.js +66 -0
  89. package/dist/__tests__/declared-proof-fixture.js.map +1 -0
  90. package/dist/__tests__/fuse-roundtrip.test.d.ts +2 -0
  91. package/dist/__tests__/fuse-roundtrip.test.d.ts.map +1 -0
  92. package/dist/__tests__/fuse-roundtrip.test.js +172 -0
  93. package/dist/__tests__/fuse-roundtrip.test.js.map +1 -0
  94. package/dist/__tests__/fuse-sdk.test.d.ts +2 -0
  95. package/dist/__tests__/fuse-sdk.test.d.ts.map +1 -0
  96. package/dist/__tests__/fuse-sdk.test.js +117 -0
  97. package/dist/__tests__/fuse-sdk.test.js.map +1 -0
  98. package/dist/__tests__/fuse-vectors.test.d.ts +2 -0
  99. package/dist/__tests__/fuse-vectors.test.d.ts.map +1 -0
  100. package/dist/__tests__/fuse-vectors.test.js +175 -0
  101. package/dist/__tests__/fuse-vectors.test.js.map +1 -0
  102. package/dist/__tests__/fuse-verify.test.d.ts +2 -0
  103. package/dist/__tests__/fuse-verify.test.d.ts.map +1 -0
  104. package/dist/__tests__/fuse-verify.test.js +250 -0
  105. package/dist/__tests__/fuse-verify.test.js.map +1 -0
  106. package/dist/__tests__/mock-proof-fixture.d.ts +15 -0
  107. package/dist/__tests__/mock-proof-fixture.d.ts.map +1 -0
  108. package/dist/__tests__/mock-proof-fixture.js +34 -0
  109. package/dist/__tests__/mock-proof-fixture.js.map +1 -0
  110. package/dist/__tests__/proof-hash-regression.test.js +4 -43
  111. package/dist/__tests__/proof-hash-regression.test.js.map +1 -1
  112. package/dist/__tests__/proof-hash.test.js +3 -21
  113. package/dist/__tests__/proof-hash.test.js.map +1 -1
  114. package/dist/__tests__/proof-integrity.test.d.ts +2 -0
  115. package/dist/__tests__/proof-integrity.test.d.ts.map +1 -0
  116. package/dist/__tests__/proof-integrity.test.js +426 -0
  117. package/dist/__tests__/proof-integrity.test.js.map +1 -0
  118. package/dist/__tests__/realistic-proof-fixture.d.ts +56 -0
  119. package/dist/__tests__/realistic-proof-fixture.d.ts.map +1 -0
  120. package/dist/__tests__/realistic-proof-fixture.js +57 -0
  121. package/dist/__tests__/realistic-proof-fixture.js.map +1 -0
  122. package/dist/__tests__/verifier.test.js +74 -0
  123. package/dist/__tests__/verifier.test.js.map +1 -1
  124. package/dist/fuse-cli.d.ts +3 -0
  125. package/dist/fuse-cli.d.ts.map +1 -0
  126. package/dist/fuse-cli.js +210 -0
  127. package/dist/fuse-cli.js.map +1 -0
  128. package/dist/fuse.d.ts +75 -0
  129. package/dist/fuse.d.ts.map +1 -0
  130. package/dist/fuse.js +273 -0
  131. package/dist/fuse.js.map +1 -0
  132. package/dist/index.d.ts +2 -0
  133. package/dist/index.d.ts.map +1 -1
  134. package/dist/index.js +4 -0
  135. package/dist/index.js.map +1 -1
  136. package/package.json +19 -9
  137. package/src/__tests__/audit-anchors.test.ts +188 -0
  138. package/src/__tests__/audit-anomalies.test.ts +815 -0
  139. package/src/__tests__/audit-attestation.test.ts +529 -0
  140. package/src/__tests__/audit-bench-smoke.test.ts +108 -0
  141. package/src/__tests__/audit-boundary.test.ts +96 -0
  142. package/src/__tests__/audit-cli.test.ts +288 -0
  143. package/src/__tests__/audit-dependencies.test.ts +141 -0
  144. package/src/__tests__/audit-fixtures.ts +736 -0
  145. package/src/__tests__/audit-ingest.test.ts +575 -0
  146. package/src/__tests__/audit-memory-ingest.test.ts +140 -0
  147. package/src/__tests__/audit-property.test.ts +892 -0
  148. package/src/__tests__/audit-real-fixtures.test.ts +153 -0
  149. package/src/__tests__/audit-real-live.test.ts +128 -0
  150. package/src/__tests__/audit-reconstruct.test.ts +353 -0
  151. package/src/__tests__/audit-report.test.ts +222 -0
  152. package/src/__tests__/audit-roundtrip.test.ts +464 -0
  153. package/src/__tests__/audit-temporal.test.ts +446 -0
  154. package/src/__tests__/audit-tiers.test.ts +343 -0
  155. package/src/__tests__/audit-witness-real.test.ts +92 -0
  156. package/src/__tests__/audit-witness.test.ts +345 -0
  157. package/src/__tests__/chain-hash-anchor.test.ts +67 -0
  158. package/src/__tests__/declared-proof-fixture.ts +65 -0
  159. package/src/__tests__/fuse-roundtrip.test.ts +174 -0
  160. package/src/__tests__/fuse-sdk.test.ts +142 -0
  161. package/src/__tests__/fuse-vectors.test.ts +218 -0
  162. package/src/__tests__/fuse-verify.test.ts +278 -0
  163. package/src/__tests__/mock-proof-fixture.ts +36 -0
  164. package/src/__tests__/proof-hash-regression.test.ts +4 -44
  165. package/src/__tests__/proof-hash.test.ts +3 -22
  166. package/src/__tests__/proof-integrity.test.ts +541 -0
  167. package/src/__tests__/real-fixtures/anchor-after-112762.json +47 -0
  168. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +47 -0
  169. package/src/__tests__/real-fixtures/live-proof-0.json +42 -0
  170. package/src/__tests__/real-fixtures/live-proof-1.json +43 -0
  171. package/src/__tests__/real-fixtures/live-proof-2.json +43 -0
  172. package/src/__tests__/real-fixtures/successor-112760.json +38 -0
  173. package/src/__tests__/real-fixtures/witness-25525146.json +6 -0
  174. package/src/__tests__/real-fixtures/witness-25525147.json +6 -0
  175. package/src/__tests__/realistic-proof-fixture.ts +57 -0
  176. package/src/__tests__/verifier.test.ts +83 -0
  177. package/src/fuse-cli.ts +187 -0
  178. package/src/fuse.ts +356 -0
  179. package/src/index.ts +6 -0
  180. package/dist/canonical.d.ts +0 -54
  181. package/dist/canonical.d.ts.map +0 -1
  182. package/dist/canonical.js +0 -119
  183. package/dist/canonical.js.map +0 -1
  184. package/dist/proof-hash.d.ts +0 -9
  185. package/dist/proof-hash.d.ts.map +0 -1
  186. package/dist/proof-hash.js +0 -75
  187. package/dist/proof-hash.js.map +0 -1
  188. package/dist/types.d.ts +0 -705
  189. package/dist/types.d.ts.map +0 -1
  190. package/dist/types.js +0 -3
  191. package/dist/types.js.map +0 -1
  192. package/dist/verifier.d.ts +0 -27
  193. package/dist/verifier.d.ts.map +0 -1
  194. package/dist/verifier.js +0 -844
  195. package/dist/verifier.js.map +0 -1
@@ -0,0 +1,153 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Real-fixture audit test (Phase 4e, G8 honesty).
5
+ *
6
+ * Builds a bundle from the repo's real embedded fixtures and asserts the
7
+ * audit reports them honestly:
8
+ *
9
+ * - REALISTIC_PROOF mirrors what the TEE actually produces: a
10
+ * real-shaped Ethereum anchor proof with a TRUNCATED attestation blob
11
+ * and a placeholder signature. It must be observed, identified as an
12
+ * Ethereum anchor by its SIGNED attribution, fail verification with
13
+ * the verifier's precise NON-version reason (the placeholder
14
+ * signature), and its attestation must report document-present-but-
15
+ * invalid (the truncated blob), never a pass and never a version
16
+ * failure.
17
+ *
18
+ * - MOCK_PROOF is placeholder values throughout; it fails on the first
19
+ * canonical check its placeholders trip (the non-base64 digest), also
20
+ * a non-version reason.
21
+ *
22
+ * The fixtures are used verbatim from their shared modules and are never
23
+ * edited.
24
+ */
25
+
26
+ import { describe, it, before, after } from "node:test";
27
+ import * as assert from "node:assert/strict";
28
+ import { rm } from "node:fs/promises";
29
+ import { computeProofHash } from "@mikeargento/bitgraph-verify";
30
+ import { buildJsonReport, runAudit } from "@mikeargento/bitgraph-audit";
31
+ import type { AuditResult } from "@mikeargento/bitgraph-audit";
32
+ import { makeTempDir, writeBundleDir } from "./audit-fixtures.js";
33
+ import { REALISTIC_PROOF } from "./realistic-proof-fixture.js";
34
+ import { MOCK_PROOF } from "./mock-proof-fixture.js";
35
+
36
+ describe("audit over the repo's real embedded fixtures", () => {
37
+ let dir: string;
38
+ let result: AuditResult;
39
+ const realisticHash = computeProofHash(REALISTIC_PROOF as unknown as Record<string, unknown>);
40
+ const mockHash = computeProofHash(MOCK_PROOF as unknown as Record<string, unknown>);
41
+
42
+ before(async () => {
43
+ dir = await makeTempDir("bitgraph-audit-real-fixtures-");
44
+ await writeBundleDir(dir, {
45
+ "proofs/realistic.json": JSON.stringify(REALISTIC_PROOF),
46
+ "proofs/mock.json": JSON.stringify(MOCK_PROOF),
47
+ });
48
+ result = await runAudit(dir);
49
+ });
50
+
51
+ after(async () => {
52
+ await rm(dir, { recursive: true, force: true });
53
+ });
54
+
55
+ it("observes both fixtures; neither is a version reject", () => {
56
+ assert.equal(result.ingest.counts.observed, 2);
57
+ assert.equal(result.ingest.counts.unsupportedVersion, 0);
58
+ const hashes = result.ingest.proofs.map((p) => p.proofHash).sort();
59
+ assert.deepEqual(hashes, [realisticHash, mockHash].sort());
60
+ });
61
+
62
+ it("REALISTIC_PROOF fails verification with the precise non-version signature reason", () => {
63
+ const proof = result.ingest.proofs.find((p) => p.proofHash === realisticHash);
64
+ assert.equal(proof?.verification?.tier, "integrity");
65
+ assert.equal(proof?.verification?.status, "failed");
66
+ assert.equal(
67
+ proof?.verification?.reason,
68
+ "signature verification failed: signature does not match"
69
+ );
70
+ assert.ok(!(proof?.verification?.reason as string).includes("version"));
71
+ });
72
+
73
+ it("MOCK_PROOF fails verification with a precise non-version placeholder reason", () => {
74
+ const proof = result.ingest.proofs.find((p) => p.proofHash === mockHash);
75
+ assert.equal(proof?.verification?.status, "failed");
76
+ assert.equal(proof?.verification?.reason, "artifact.digestB64 is not valid base64");
77
+ assert.ok(!(proof?.verification?.reason as string).includes("version"));
78
+ });
79
+
80
+ it("nothing passes: no proof is ever reported verified or artifact-unavailable", () => {
81
+ assert.equal(result.verification.verified, 0);
82
+ assert.equal(result.verification.artifactUnavailable, 0);
83
+ assert.equal(result.verification.failed, 2);
84
+ });
85
+
86
+ it("REALISTIC_PROOF is identified as an Ethereum anchor by its signed attribution", () => {
87
+ assert.equal(result.anchors.anchors.length, 1);
88
+ const anchor = result.anchors.anchors[0]!;
89
+ assert.equal(anchor.proofHash, realisticHash);
90
+ assert.equal(anchor.blockNumber, "24800448");
91
+ assert.equal(
92
+ anchor.blockHash,
93
+ "0x28ed3639cd705fb8cb2b915c1991e9f808b40e775bc8eb540702942729fec2c0"
94
+ );
95
+ // No unsigned metadata on the fixture: absent corroboration, not disagreement.
96
+ assert.equal(anchor.metadataCorroboration, "absent");
97
+ // The verification failure rides along unmodified.
98
+ assert.equal(anchor.verificationStatus, "failed");
99
+ // MOCK_PROOF's attribution ("test") never identifies an anchor.
100
+ assert.ok(!result.anchors.anchors.some((a) => a.proofHash === mockHash));
101
+ // No witness in the bundle: causal identification only, no wall-clock evidence.
102
+ assert.deepEqual(result.temporal.verifiedAnchorProofHashes, []);
103
+ assert.deepEqual(result.temporal.unverifiedAnchorProofHashes, [realisticHash]);
104
+ });
105
+
106
+ it("REALISTIC_PROOF attestation reports document-present-but-invalid, never a pass", () => {
107
+ const record = result.attestations.records.find((r) => r.proofHash === realisticHash);
108
+ assert.ok(record !== undefined);
109
+ assert.equal(record.declaredMeasurementPresent, true);
110
+ assert.equal(
111
+ record.declaredMeasurement,
112
+ "638d655ad6091bed5c358628b7780de0cdbe138a37fe09d52bf8021a720680a2b3c730fee9f6bef79c1dbe68ef3cdd94"
113
+ );
114
+ assert.equal(record.attestationFormat, "aws-nitro");
115
+ assert.equal(record.documentPresent, true);
116
+ assert.equal(record.documentValidated, false);
117
+ // The truncated blob fails with a precise reason, never silently.
118
+ assert.equal(typeof record.validationFailure, "string");
119
+ assert.ok((record.validationFailure as string).length > 0);
120
+ // Facts derived from an unvalidated document are never asserted.
121
+ assert.equal(record.pcr0MatchesDeclared, undefined);
122
+ assert.equal(record.userDataBoundToProof, undefined);
123
+ // Aggregates: documents present on both fixtures, zero validated.
124
+ assert.equal(result.attestations.counts.proofsWithDocument, 2);
125
+ assert.equal(result.attestations.counts.documentsValidated, 0);
126
+ assert.equal(result.attestations.counts.documentsFailed, 2);
127
+ assert.equal(result.attestations.counts.pcr0Matches, 0);
128
+ assert.equal(result.attestations.counts.userDataBound, 0);
129
+ });
130
+
131
+ it("the exact anomaly code multiset is honest: crypto and topology findings, no version failure", () => {
132
+ const report = buildJsonReport(result);
133
+ const codes = report.anomalies.map((a) => a.code).sort();
134
+ assert.deepEqual(codes, [
135
+ "attestation-invalid", // MOCK_PROOF's placeholder attestation blob
136
+ "attestation-invalid", // REALISTIC_PROOF's truncated attestation blob
137
+ "chain-break-malformed", // MOCK_PROOF's placeholder prevB64 ("prev==")
138
+ ]);
139
+ // REALISTIC_PROOF's real-shaped prevB64 into unobserved history is the
140
+ // frontier of its (single-proof) partition, so it is an expected bundle
141
+ // boundary, not a chain-integrity anomaly.
142
+ assert.equal(report.boundaryEntryPoints.length, 1);
143
+ assert.ok(!codes.includes("unsupported-version"));
144
+ assert.equal(report.summary.fullyVerified, 0);
145
+ // Still exit 3: attestation-invalid trips verification, chain-break-malformed
146
+ // trips chain anomalies. The boundary alone would never set an exit bit.
147
+ assert.deepEqual(report.summary.exit, {
148
+ verificationFailures: true,
149
+ chainAnomaliesOrDivergences: true,
150
+ code: 3,
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,128 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Real-ledger regression test for the audit pipeline.
5
+ *
6
+ * These three proofs (real-fixtures/live-proof-0.json .. live-proof-2.json)
7
+ * were captured from the live BitGraph ledger: a contiguous single-epoch
8
+ * chain (genesis commit counter 2, then 4, then 6, each linked to its
9
+ * predecessor by commit.prevB64). They are stored-form, carrying the
10
+ * ledger-added proofHash field.
11
+ *
12
+ * This test exists specifically because the synthetic fixtures shared the
13
+ * audit tool's ORIGINAL wrong assumptions and so masked two real bugs:
14
+ *
15
+ * Bug A: real slot allocations carry NO time field (the enclave builds
16
+ * slot bodies without a clock). The verifier used to require it, so real
17
+ * slots failed verification. If this test's proofs verify at the
18
+ * integrity tier (0 failed), Bug A is fixed.
19
+ *
20
+ * Bug B: commit.prevB64 references computeChainHash(predecessor) (SHA-256
21
+ * over the whole canonical proof minus the ledger proofHash), NOT
22
+ * computeProofHash (the signed-body subset). The synthetic fixtures built
23
+ * their prevB64 from computeProofHash, the same wrong value the tool
24
+ * assumed, so they linked "by accident". Real proofs use the true chain
25
+ * hash. If this real chain reconstructs as ONE intact component with ZERO
26
+ * anomalies, Bug B is fixed.
27
+ *
28
+ * No synthetic construction: the assertions ride entirely on real bytes.
29
+ */
30
+
31
+ import { describe, it, before, after } from "node:test";
32
+ import * as assert from "node:assert/strict";
33
+ import { mkdtemp, mkdir, writeFile, rm, readFile } from "node:fs/promises";
34
+ import { tmpdir } from "node:os";
35
+ import { join } from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+ import {
38
+ runAudit,
39
+ buildJsonReport,
40
+ computeExitFlags,
41
+ type AuditResult,
42
+ } from "@mikeargento/bitgraph-audit";
43
+ import { computeProofHash, type BitGraphProof } from "@mikeargento/bitgraph-verify";
44
+
45
+ /** Filesystem-safe token from a base64/arbitrary string. */
46
+ function safe(value: string): string {
47
+ return value.replace(/[^A-Za-z0-9]/g, "_");
48
+ }
49
+
50
+ describe("audit: real live-ledger chain (Bug A + Bug B regression)", () => {
51
+ let dir: string;
52
+ let result: AuditResult;
53
+ let proofs: Array<BitGraphProof & { proofHash?: string }>;
54
+
55
+ before(async () => {
56
+ // Load the three captured real proofs from src/__tests__/real-fixtures.
57
+ // The test runs from dist/__tests__, so reach back into src.
58
+ proofs = [];
59
+ for (const i of [0, 1, 2]) {
60
+ const url = new URL(
61
+ `../../src/__tests__/real-fixtures/live-proof-${i}.json`,
62
+ import.meta.url
63
+ );
64
+ const raw = await readFile(fileURLToPath(url), "utf8");
65
+ proofs.push(JSON.parse(raw) as BitGraphProof & { proofHash?: string });
66
+ }
67
+
68
+ // Write a valid directory bundle: proofs/<epoch>/<counter>-<hash>.json.
69
+ // Bytes are the exact captured stored-form JSON; no re-serialization of
70
+ // the signed content, so nothing about the real proof is altered.
71
+ dir = await mkdtemp(join(tmpdir(), "bitgraph-audit-real-live-"));
72
+ for (const proof of proofs) {
73
+ const epochDir = join(dir, "proofs", safe(proof.commit.epochId as string));
74
+ await mkdir(epochDir, { recursive: true });
75
+ const name = `${proof.commit.counter}-${safe(computeProofHash(proof))}.json`;
76
+ await writeFile(join(epochDir, name), JSON.stringify(proof));
77
+ }
78
+
79
+ result = await runAudit(dir);
80
+ });
81
+
82
+ after(async () => {
83
+ if (dir !== undefined) await rm(dir, { recursive: true, force: true });
84
+ });
85
+
86
+ it("verifies every real proof at the integrity tier (Bug A: clockless slots verify)", () => {
87
+ // All three are observed, none rejected as an unsupported version.
88
+ assert.equal(result.ingest.counts.observed, 3);
89
+ assert.equal(result.ingest.counts.unsupportedVersion, 0);
90
+
91
+ // Bug A proof: real slot allocations carry no time field. If the
92
+ // verifier still required it, these would be "failed", not
93
+ // "artifact-unavailable". No artifact bytes are in the bundle, so the
94
+ // integrity tier is the ceiling and every proof must pass it.
95
+ assert.equal(result.verification.failed, 0);
96
+ assert.equal(result.verification.verified, 0);
97
+ assert.equal(result.verification.artifactUnavailable, 3);
98
+ });
99
+
100
+ it("reconstructs the real chain as one intact component (Bug B: prevB64 is the chain hash)", () => {
101
+ const report = buildJsonReport(result);
102
+
103
+ // One signer, one epoch, one chain -> exactly one partition.
104
+ assert.equal(result.reconstruction.partitions.length, 1);
105
+ const partition = result.reconstruction.partitions[0]!;
106
+
107
+ // The three proofs link into a SINGLE connected component of all three
108
+ // members. This only holds if commit.prevB64 resolves against
109
+ // computeChainHash(predecessor); against computeProofHash it would
110
+ // fragment into three genesis components with chain breaks.
111
+ assert.equal(partition.components.length, 1);
112
+ assert.equal(partition.components[0]!.memberProofHashes.length, 3);
113
+
114
+ // No anomalies of any kind, no divergences, chain intact.
115
+ assert.equal(result.anomalies.anomalies.length, 0);
116
+ assert.equal(result.anomalies.divergences.length, 0);
117
+ assert.equal(report.summary.chainIntact, true);
118
+ assert.equal(report.summary.partitionsIntact, 1);
119
+ });
120
+
121
+ it("exits clean (code 0) over the real bundle", () => {
122
+ assert.deepEqual(computeExitFlags(result), {
123
+ verificationFailures: false,
124
+ chainAnomaliesOrDivergences: false,
125
+ code: 0,
126
+ });
127
+ });
128
+ });
@@ -0,0 +1,353 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Tests for @mikeargento/bitgraph-audit causal reconstruction:
5
+ * partitioning per (signer key, epochId, chainId), chain components from
6
+ * prevB64 hash links, genesis and terminal identification, counter
7
+ * evidence, and epoch relationships (epochLink lineage edges, hard-edge
8
+ * ordering, observed-but-unordered epochs).
9
+ */
10
+
11
+ import { describe, test, after } from "node:test";
12
+ import * as assert from "node:assert/strict";
13
+ import { rm } from "node:fs/promises";
14
+ import {
15
+ classifyAnomalies,
16
+ ingestBundle,
17
+ reconstructChains,
18
+ verifyObservedProofs,
19
+ } from "@mikeargento/bitgraph-audit";
20
+ import type { IngestResult, ReconstructionResult, AnomalyReport } from "@mikeargento/bitgraph-audit";
21
+ import { sha256 } from "@noble/hashes/sha256";
22
+ import {
23
+ b64,
24
+ healthyPairs,
25
+ makeConstructorProof,
26
+ makeCounterChain,
27
+ makeEpochLinkProof,
28
+ makeKey,
29
+ makeTempDir,
30
+ proofJson,
31
+ utf8,
32
+ writeBundleDir,
33
+ } from "./audit-fixtures.js";
34
+
35
+ const tempDirs: string[] = [];
36
+
37
+ after(async () => {
38
+ for (const dir of tempDirs) {
39
+ await rm(dir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ async function auditBundle(files: Record<string, Uint8Array | string>): Promise<{
44
+ ingest: IngestResult;
45
+ reconstruction: ReconstructionResult;
46
+ report: AnomalyReport;
47
+ }> {
48
+ const dir = await makeTempDir("bitgraph-audit-reconstruct-");
49
+ tempDirs.push(dir);
50
+ await writeBundleDir(dir, files);
51
+ const ingest = await ingestBundle(dir);
52
+ await verifyObservedProofs(ingest);
53
+ const reconstruction = await reconstructChains(ingest);
54
+ const report = await classifyAnomalies(ingest, reconstruction);
55
+ return { ingest, reconstruction, report };
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // The critical regression: a healthy chain is clean
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("reconstruct: healthy slot/commit chain", () => {
63
+ test("one partition, one component, zero anomalies, zero unexplained positions", async () => {
64
+ const chain = await makeCounterChain({
65
+ epochId: "epoch-healthy",
66
+ chainId: "bitgraph:main",
67
+ pairs: healthyPairs(4), // slot 1/commit 2 ... slot 7/commit 8
68
+ });
69
+ const { ingest, reconstruction, report } = await auditBundle({
70
+ "proofs/p0.json": proofJson(chain.proofs[0]!.proof),
71
+ "proofs/p1.json": proofJson(chain.proofs[1]!.proof),
72
+ "proofs/p2.json": proofJson(chain.proofs[2]!.proof),
73
+ "proofs/p3.json": proofJson(chain.proofs[3]!.proof),
74
+ });
75
+
76
+ assert.equal(ingest.proofs.length, 4);
77
+ assert.equal(reconstruction.partitions.length, 1);
78
+ const partition = reconstruction.partitions[0]!;
79
+ assert.deepEqual(partition.key, {
80
+ publicKeyB64: chain.key.publicKeyB64,
81
+ epochId: "epoch-healthy",
82
+ chainId: "bitgraph:main",
83
+ });
84
+
85
+ assert.equal(partition.components.length, 1);
86
+ const component = partition.components[0]!;
87
+ assert.deepEqual(
88
+ component.memberProofHashes,
89
+ chain.proofs.map((p) => p.proofHash),
90
+ "link order must follow the hash chain from genesis to terminal"
91
+ );
92
+ assert.deepEqual(component.genesisProofHashes, [chain.proofs[0]!.proofHash]);
93
+ assert.deepEqual(component.terminalProofHashes, [chain.proofs[3]!.proofHash]);
94
+ assert.deepEqual(component.brokenLinkProofHashes, []);
95
+ assert.equal(component.hasCounterEvidence, true);
96
+ assert.deepEqual(component.positionRange, { min: "1", max: "8" });
97
+
98
+ // THE critical regression: a healthy slot/commit chain produces NO
99
+ // unexplained positions and no anomalies of any kind.
100
+ assert.deepEqual(report.anomalies, []);
101
+ assert.deepEqual(report.divergences, []);
102
+ });
103
+
104
+ test("reconstruction is link-driven: file order and counters are not the mechanism", async () => {
105
+ const chain = await makeCounterChain({
106
+ epochId: "epoch-shuffled",
107
+ pairs: healthyPairs(3),
108
+ });
109
+ // Write files in an order that reverses the causal order.
110
+ const { reconstruction } = await auditBundle({
111
+ "a-last.json": proofJson(chain.proofs[2]!.proof),
112
+ "b-middle.json": proofJson(chain.proofs[1]!.proof),
113
+ "c-first.json": proofJson(chain.proofs[0]!.proof),
114
+ });
115
+ const component = reconstruction.partitions[0]!.components[0]!;
116
+ assert.deepEqual(
117
+ component.memberProofHashes,
118
+ chain.proofs.map((p) => p.proofHash)
119
+ );
120
+ });
121
+
122
+ test("a counterless link chain still reconstructs as one component", async () => {
123
+ const chain = await makeCounterChain({
124
+ epochId: "epoch-no-counters",
125
+ pairs: [{}, {}, {}], // epochId only: hash links are the only evidence
126
+ });
127
+ const { reconstruction, report } = await auditBundle({
128
+ "p0.json": proofJson(chain.proofs[0]!.proof),
129
+ "p1.json": proofJson(chain.proofs[1]!.proof),
130
+ "p2.json": proofJson(chain.proofs[2]!.proof),
131
+ });
132
+ const partition = reconstruction.partitions[0]!;
133
+ assert.equal(partition.components.length, 1);
134
+ const component = partition.components[0]!;
135
+ assert.deepEqual(
136
+ component.memberProofHashes,
137
+ chain.proofs.map((p) => p.proofHash)
138
+ );
139
+ assert.equal(component.hasCounterEvidence, false);
140
+ assert.equal(component.positionRange, undefined);
141
+ assert.deepEqual(report.anomalies, []);
142
+ });
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Partitioning
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe("reconstruct: partitioning", () => {
150
+ test("two independent lineages are never merged", async () => {
151
+ const chainA = await makeCounterChain({ epochId: "epoch-A", pairs: healthyPairs(2) });
152
+ const chainB = await makeCounterChain({ epochId: "epoch-B", pairs: healthyPairs(2) });
153
+ const { reconstruction, report } = await auditBundle({
154
+ "a0.json": proofJson(chainA.proofs[0]!.proof),
155
+ "a1.json": proofJson(chainA.proofs[1]!.proof),
156
+ "b0.json": proofJson(chainB.proofs[0]!.proof),
157
+ "b1.json": proofJson(chainB.proofs[1]!.proof),
158
+ });
159
+
160
+ assert.equal(reconstruction.partitions.length, 2);
161
+ const keys = reconstruction.partitions.map((p) => p.key.publicKeyB64).sort();
162
+ assert.deepEqual(keys, [chainA.key.publicKeyB64, chainB.key.publicKeyB64].sort());
163
+ // Same counters on both chains, yet no collisions: counters are
164
+ // partition-local.
165
+ assert.deepEqual(report.anomalies, []);
166
+ assert.deepEqual(report.divergences, []);
167
+ });
168
+
169
+ test("same signer and epoch on different chainIds are separate partitions (G6)", async () => {
170
+ const key = await makeKey();
171
+ const main = await makeCounterChain({
172
+ key,
173
+ epochId: "epoch-multichain",
174
+ chainId: "bitgraph:main",
175
+ pairs: healthyPairs(2),
176
+ payloadPrefix: "main",
177
+ });
178
+ const globalChain = await makeCounterChain({
179
+ key,
180
+ epochId: "epoch-multichain",
181
+ pairs: healthyPairs(2), // no chainId: normalized "global"
182
+ payloadPrefix: "global",
183
+ });
184
+ const { reconstruction, report } = await auditBundle({
185
+ "m0.json": proofJson(main.proofs[0]!.proof),
186
+ "m1.json": proofJson(main.proofs[1]!.proof),
187
+ "g0.json": proofJson(globalChain.proofs[0]!.proof),
188
+ "g1.json": proofJson(globalChain.proofs[1]!.proof),
189
+ });
190
+
191
+ assert.equal(reconstruction.partitions.length, 2);
192
+ assert.deepEqual(
193
+ reconstruction.partitions.map((p) => p.key.chainId).sort(),
194
+ ["bitgraph:main", "global"]
195
+ );
196
+ assert.deepEqual(report.anomalies, []);
197
+ });
198
+
199
+ test("chainless proofs stay outside partitions as observed-but-unchained", async () => {
200
+ const chain = await makeCounterChain({ epochId: "epoch-with-stray", pairs: healthyPairs(2) });
201
+ const chainless = await makeConstructorProof(); // no counter, no epochId
202
+ const { ingest, reconstruction, report } = await auditBundle({
203
+ "p0.json": proofJson(chain.proofs[0]!.proof),
204
+ "p1.json": proofJson(chain.proofs[1]!.proof),
205
+ "stray.json": proofJson(chainless.proof),
206
+ });
207
+
208
+ assert.equal(ingest.proofs.length, 3);
209
+ assert.equal(reconstruction.partitions.length, 1);
210
+ assert.equal(reconstruction.partitions[0]!.memberProofHashes.length, 2);
211
+ const strayHash = ingest.proofs.find((p) => p.chainless)!.proofHash;
212
+ assert.deepEqual(reconstruction.unchainedProofHashes, [strayHash]);
213
+ // Observed-but-unchained is not an anomaly.
214
+ assert.deepEqual(report.anomalies, []);
215
+ });
216
+
217
+ test("a proof with chain fields but no signer key is unpartitioned", async () => {
218
+ const broken = JSON.stringify({
219
+ version: "bitgraph/1",
220
+ artifact: { hashAlg: "sha256", digestB64: b64(sha256(utf8("broken-payload"))) },
221
+ commit: { nonceB64: "AAAA", counter: "2", epochId: "epoch-broken" },
222
+ signer: { publicKeyB64: 42, signatureB64: "AAAA" },
223
+ });
224
+ const { ingest, reconstruction } = await auditBundle({ "broken.json": broken });
225
+ assert.equal(ingest.proofs.length, 1);
226
+ assert.equal(reconstruction.partitions.length, 0);
227
+ assert.deepEqual(reconstruction.unpartitionedProofHashes, [ingest.proofs[0]!.proofHash]);
228
+ // The verification dimension reports the structural failure.
229
+ assert.equal(ingest.proofs[0]!.verification!.status, "failed");
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Missing proofs split components
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("reconstruct: missing middle proof", () => {
238
+ test("splits the partition into two components with the break identified", async () => {
239
+ const chain = await makeCounterChain({ epochId: "epoch-gap", pairs: healthyPairs(3) });
240
+ // Omit the middle proof (slot 3 / commit 4).
241
+ const { reconstruction } = await auditBundle({
242
+ "p0.json": proofJson(chain.proofs[0]!.proof),
243
+ "p2.json": proofJson(chain.proofs[2]!.proof),
244
+ });
245
+
246
+ const partition = reconstruction.partitions[0]!;
247
+ assert.equal(partition.components.length, 2);
248
+ const [first, second] = partition.components;
249
+ assert.deepEqual(first!.memberProofHashes, [chain.proofs[0]!.proofHash]);
250
+ assert.deepEqual(first!.genesisProofHashes, [chain.proofs[0]!.proofHash]);
251
+ assert.deepEqual(first!.terminalProofHashes, [chain.proofs[0]!.proofHash]);
252
+ assert.deepEqual(second!.memberProofHashes, [chain.proofs[2]!.proofHash]);
253
+ assert.deepEqual(second!.genesisProofHashes, []);
254
+ assert.deepEqual(second!.brokenLinkProofHashes, [chain.proofs[2]!.proofHash]);
255
+ });
256
+ });
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Epoch relationships
260
+ // ---------------------------------------------------------------------------
261
+
262
+ describe("reconstruct: epoch relationships", () => {
263
+ test("a valid epochLink produces a hard lineage edge and linked ordering", async () => {
264
+ const chainA = await makeCounterChain({ epochId: "epoch-lineage-A", pairs: healthyPairs(2) });
265
+ const terminalA = chainA.proofs[1]!;
266
+ const genesisB = await makeEpochLinkProof({
267
+ prevEpochId: "epoch-lineage-A",
268
+ prevCounter: terminalA.proof.commit.counter as string,
269
+ prevProofHashB64: terminalA.chainHash,
270
+ prevPublicKeyB64: chainA.key.publicKeyB64,
271
+ toEpochId: "epoch-lineage-B",
272
+ counter: "2",
273
+ slotCounter: "1",
274
+ });
275
+
276
+ const { reconstruction, report } = await auditBundle({
277
+ "a0.json": proofJson(chainA.proofs[0]!.proof),
278
+ "a1.json": proofJson(terminalA.proof),
279
+ "b-genesis.json": proofJson(genesisB.proof),
280
+ });
281
+
282
+ const { edges, epochs, orderedPairs } = reconstruction.epochRelationships;
283
+ assert.equal(edges.length, 1);
284
+ const edge = edges[0]!;
285
+ assert.equal(edge.resolution, "matched");
286
+ assert.equal(edge.fromEpochId, "epoch-lineage-A");
287
+ assert.equal(edge.toEpochId, "epoch-lineage-B");
288
+ assert.equal(edge.predecessorProofHash, terminalA.proofHash);
289
+ assert.equal(edge.metadataConsistent, true);
290
+ assert.equal(edge.referencedProofIsTerminal, true);
291
+ assert.equal(edge.viaProofValid, true);
292
+ assert.equal(edge.predecessorValid, true);
293
+ assert.equal(edge.hardEdge, true);
294
+
295
+ assert.deepEqual(orderedPairs, [
296
+ { beforeEpochId: "epoch-lineage-A", afterEpochId: "epoch-lineage-B" },
297
+ ]);
298
+ const epochA = epochs.find((e) => e.epochId === "epoch-lineage-A")!;
299
+ const epochB = epochs.find((e) => e.epochId === "epoch-lineage-B")!;
300
+ assert.equal(epochA.ordering, "linked");
301
+ assert.equal(epochB.ordering, "linked");
302
+ assert.deepEqual(epochA.linkedSuccessorEpochIds, ["epoch-lineage-B"]);
303
+ assert.deepEqual(epochB.linkedPredecessorEpochIds, ["epoch-lineage-A"]);
304
+ // The extension point stays untouched by reconstruction.
305
+ assert.equal(epochA.anchorBounds, undefined);
306
+
307
+ // Valid lineage is not an anomaly.
308
+ assert.deepEqual(report.anomalies, []);
309
+ assert.deepEqual(report.divergences, []);
310
+ });
311
+
312
+ test("a dangling epochLink yields no hard edge and no ordering", async () => {
313
+ const genesis = await makeEpochLinkProof({
314
+ prevEpochId: "epoch-never-observed",
315
+ prevCounter: "10",
316
+ prevProofHashB64: b64(sha256(utf8("terminal-never-observed"))),
317
+ toEpochId: "epoch-dangling-B",
318
+ counter: "2",
319
+ slotCounter: "1",
320
+ });
321
+ const { reconstruction } = await auditBundle({
322
+ "b-genesis.json": proofJson(genesis.proof),
323
+ });
324
+
325
+ const { edges, epochs, orderedPairs } = reconstruction.epochRelationships;
326
+ assert.equal(edges.length, 1);
327
+ assert.equal(edges[0]!.resolution, "dangling");
328
+ assert.equal(edges[0]!.hardEdge, false);
329
+ assert.deepEqual(orderedPairs, []);
330
+ assert.equal(epochs.find((e) => e.epochId === "epoch-dangling-B")!.ordering, "observed-but-unordered");
331
+ });
332
+
333
+ test("epochs with no lineage evidence are observed-but-unordered, never divergence", async () => {
334
+ const chainA = await makeCounterChain({ epochId: "epoch-solo-A", pairs: healthyPairs(2) });
335
+ const chainB = await makeCounterChain({ epochId: "epoch-solo-B", pairs: healthyPairs(2) });
336
+ const { reconstruction, report } = await auditBundle({
337
+ "a0.json": proofJson(chainA.proofs[0]!.proof),
338
+ "a1.json": proofJson(chainA.proofs[1]!.proof),
339
+ "b0.json": proofJson(chainB.proofs[0]!.proof),
340
+ "b1.json": proofJson(chainB.proofs[1]!.proof),
341
+ });
342
+
343
+ const { epochs, edges, orderedPairs } = reconstruction.epochRelationships;
344
+ assert.equal(edges.length, 0);
345
+ assert.deepEqual(orderedPairs, []);
346
+ for (const epoch of epochs) {
347
+ assert.equal(epoch.ordering, "observed-but-unordered");
348
+ }
349
+ // Concurrent-or-unordered is never divergence.
350
+ assert.deepEqual(report.divergences, []);
351
+ assert.deepEqual(report.anomalies, []);
352
+ });
353
+ });