@mikeargento/bitgraph 1.9.1 → 1.9.2

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 (239) hide show
  1. package/package.json +9 -6
  2. package/dist/__tests__/audit-anchors.test.d.ts +0 -2
  3. package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
  4. package/dist/__tests__/audit-anchors.test.js +0 -152
  5. package/dist/__tests__/audit-anchors.test.js.map +0 -1
  6. package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
  7. package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
  8. package/dist/__tests__/audit-anomalies.test.js +0 -663
  9. package/dist/__tests__/audit-anomalies.test.js.map +0 -1
  10. package/dist/__tests__/audit-attestation.test.d.ts +0 -2
  11. package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
  12. package/dist/__tests__/audit-attestation.test.js +0 -403
  13. package/dist/__tests__/audit-attestation.test.js.map +0 -1
  14. package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
  15. package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
  16. package/dist/__tests__/audit-bench-smoke.test.js +0 -87
  17. package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
  18. package/dist/__tests__/audit-boundary.test.d.ts +0 -20
  19. package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
  20. package/dist/__tests__/audit-boundary.test.js +0 -84
  21. package/dist/__tests__/audit-boundary.test.js.map +0 -1
  22. package/dist/__tests__/audit-cli.test.d.ts +0 -2
  23. package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
  24. package/dist/__tests__/audit-cli.test.js +0 -239
  25. package/dist/__tests__/audit-cli.test.js.map +0 -1
  26. package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
  27. package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
  28. package/dist/__tests__/audit-dependencies.test.js +0 -116
  29. package/dist/__tests__/audit-dependencies.test.js.map +0 -1
  30. package/dist/__tests__/audit-fixtures.d.ts +0 -265
  31. package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
  32. package/dist/__tests__/audit-fixtures.js +0 -511
  33. package/dist/__tests__/audit-fixtures.js.map +0 -1
  34. package/dist/__tests__/audit-ingest.test.d.ts +0 -2
  35. package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
  36. package/dist/__tests__/audit-ingest.test.js +0 -473
  37. package/dist/__tests__/audit-ingest.test.js.map +0 -1
  38. package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
  39. package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
  40. package/dist/__tests__/audit-memory-ingest.test.js +0 -135
  41. package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
  42. package/dist/__tests__/audit-property.test.d.ts +0 -2
  43. package/dist/__tests__/audit-property.test.d.ts.map +0 -1
  44. package/dist/__tests__/audit-property.test.js +0 -771
  45. package/dist/__tests__/audit-property.test.js.map +0 -1
  46. package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
  47. package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
  48. package/dist/__tests__/audit-real-fixtures.test.js +0 -132
  49. package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
  50. package/dist/__tests__/audit-real-live.test.d.ts +0 -2
  51. package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
  52. package/dist/__tests__/audit-real-live.test.js +0 -107
  53. package/dist/__tests__/audit-real-live.test.js.map +0 -1
  54. package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
  55. package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
  56. package/dist/__tests__/audit-reconstruct.test.js +0 -287
  57. package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
  58. package/dist/__tests__/audit-report.test.d.ts +0 -2
  59. package/dist/__tests__/audit-report.test.d.ts.map +0 -1
  60. package/dist/__tests__/audit-report.test.js +0 -186
  61. package/dist/__tests__/audit-report.test.js.map +0 -1
  62. package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
  63. package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
  64. package/dist/__tests__/audit-roundtrip.test.js +0 -368
  65. package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
  66. package/dist/__tests__/audit-temporal.test.d.ts +0 -2
  67. package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
  68. package/dist/__tests__/audit-temporal.test.js +0 -364
  69. package/dist/__tests__/audit-temporal.test.js.map +0 -1
  70. package/dist/__tests__/audit-tiers.test.d.ts +0 -2
  71. package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
  72. package/dist/__tests__/audit-tiers.test.js +0 -282
  73. package/dist/__tests__/audit-tiers.test.js.map +0 -1
  74. package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
  75. package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
  76. package/dist/__tests__/audit-witness-real.test.js +0 -82
  77. package/dist/__tests__/audit-witness-real.test.js.map +0 -1
  78. package/dist/__tests__/audit-witness.test.d.ts +0 -2
  79. package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
  80. package/dist/__tests__/audit-witness.test.js +0 -308
  81. package/dist/__tests__/audit-witness.test.js.map +0 -1
  82. package/dist/__tests__/canonical.test.d.ts +0 -2
  83. package/dist/__tests__/canonical.test.d.ts.map +0 -1
  84. package/dist/__tests__/canonical.test.js +0 -162
  85. package/dist/__tests__/canonical.test.js.map +0 -1
  86. package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
  87. package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
  88. package/dist/__tests__/chain-hash-anchor.test.js +0 -55
  89. package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
  90. package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
  91. package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
  92. package/dist/__tests__/commit-service.integration.test.js +0 -95
  93. package/dist/__tests__/commit-service.integration.test.js.map +0 -1
  94. package/dist/__tests__/constructor.test.d.ts +0 -2
  95. package/dist/__tests__/constructor.test.d.ts.map +0 -1
  96. package/dist/__tests__/constructor.test.js +0 -267
  97. package/dist/__tests__/constructor.test.js.map +0 -1
  98. package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
  99. package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
  100. package/dist/__tests__/declared-proof-fixture.js +0 -66
  101. package/dist/__tests__/declared-proof-fixture.js.map +0 -1
  102. package/dist/__tests__/fuse-inline.test.d.ts +0 -2
  103. package/dist/__tests__/fuse-inline.test.d.ts.map +0 -1
  104. package/dist/__tests__/fuse-inline.test.js +0 -181
  105. package/dist/__tests__/fuse-inline.test.js.map +0 -1
  106. package/dist/__tests__/fuse-placement.test.d.ts +0 -2
  107. package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
  108. package/dist/__tests__/fuse-placement.test.js +0 -31
  109. package/dist/__tests__/fuse-placement.test.js.map +0 -1
  110. package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
  111. package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
  112. package/dist/__tests__/fuse-roundtrip.test.js +0 -241
  113. package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
  114. package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
  115. package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
  116. package/dist/__tests__/fuse-sdk.test.js +0 -117
  117. package/dist/__tests__/fuse-sdk.test.js.map +0 -1
  118. package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
  119. package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
  120. package/dist/__tests__/fuse-set-sdk.test.js +0 -721
  121. package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
  122. package/dist/__tests__/fuse-set.test.d.ts +0 -2
  123. package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
  124. package/dist/__tests__/fuse-set.test.js +0 -889
  125. package/dist/__tests__/fuse-set.test.js.map +0 -1
  126. package/dist/__tests__/fuse-set2.test.d.ts +0 -2
  127. package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
  128. package/dist/__tests__/fuse-set2.test.js +0 -294
  129. package/dist/__tests__/fuse-set2.test.js.map +0 -1
  130. package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
  131. package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
  132. package/dist/__tests__/fuse-vectors.test.js +0 -175
  133. package/dist/__tests__/fuse-vectors.test.js.map +0 -1
  134. package/dist/__tests__/fuse-verify.test.d.ts +0 -2
  135. package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
  136. package/dist/__tests__/fuse-verify.test.js +0 -250
  137. package/dist/__tests__/fuse-verify.test.js.map +0 -1
  138. package/dist/__tests__/hello-world.test.d.ts +0 -2
  139. package/dist/__tests__/hello-world.test.d.ts.map +0 -1
  140. package/dist/__tests__/hello-world.test.js +0 -9
  141. package/dist/__tests__/hello-world.test.js.map +0 -1
  142. package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
  143. package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
  144. package/dist/__tests__/mock-proof-fixture.js +0 -34
  145. package/dist/__tests__/mock-proof-fixture.js.map +0 -1
  146. package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
  147. package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
  148. package/dist/__tests__/policy-enforcement.test.js +0 -188
  149. package/dist/__tests__/policy-enforcement.test.js.map +0 -1
  150. package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
  151. package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
  152. package/dist/__tests__/proof-hash-regression.test.js +0 -95
  153. package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
  154. package/dist/__tests__/proof-hash.test.d.ts +0 -2
  155. package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
  156. package/dist/__tests__/proof-hash.test.js +0 -116
  157. package/dist/__tests__/proof-hash.test.js.map +0 -1
  158. package/dist/__tests__/proof-integrity.test.d.ts +0 -2
  159. package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
  160. package/dist/__tests__/proof-integrity.test.js +0 -454
  161. package/dist/__tests__/proof-integrity.test.js.map +0 -1
  162. package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
  163. package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
  164. package/dist/__tests__/realistic-proof-fixture.js +0 -57
  165. package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
  166. package/dist/__tests__/run.test.d.ts +0 -2
  167. package/dist/__tests__/run.test.d.ts.map +0 -1
  168. package/dist/__tests__/run.test.js +0 -178
  169. package/dist/__tests__/run.test.js.map +0 -1
  170. package/dist/__tests__/verifier.test.d.ts +0 -2
  171. package/dist/__tests__/verifier.test.d.ts.map +0 -1
  172. package/dist/__tests__/verifier.test.js +0 -531
  173. package/dist/__tests__/verifier.test.js.map +0 -1
  174. package/dist/canonical.d.ts +0 -54
  175. package/dist/canonical.d.ts.map +0 -1
  176. package/dist/canonical.js +0 -119
  177. package/dist/canonical.js.map +0 -1
  178. package/dist/proof-hash.d.ts +0 -9
  179. package/dist/proof-hash.d.ts.map +0 -1
  180. package/dist/proof-hash.js +0 -75
  181. package/dist/proof-hash.js.map +0 -1
  182. package/dist/types.d.ts +0 -705
  183. package/dist/types.d.ts.map +0 -1
  184. package/dist/types.js +0 -3
  185. package/dist/types.js.map +0 -1
  186. package/dist/verifier.d.ts +0 -27
  187. package/dist/verifier.d.ts.map +0 -1
  188. package/dist/verifier.js +0 -844
  189. package/dist/verifier.js.map +0 -1
  190. package/src/__tests__/audit-anchors.test.ts +0 -188
  191. package/src/__tests__/audit-anomalies.test.ts +0 -815
  192. package/src/__tests__/audit-attestation.test.ts +0 -529
  193. package/src/__tests__/audit-bench-smoke.test.ts +0 -108
  194. package/src/__tests__/audit-boundary.test.ts +0 -96
  195. package/src/__tests__/audit-cli.test.ts +0 -288
  196. package/src/__tests__/audit-dependencies.test.ts +0 -141
  197. package/src/__tests__/audit-fixtures.ts +0 -736
  198. package/src/__tests__/audit-ingest.test.ts +0 -575
  199. package/src/__tests__/audit-memory-ingest.test.ts +0 -156
  200. package/src/__tests__/audit-property.test.ts +0 -892
  201. package/src/__tests__/audit-real-fixtures.test.ts +0 -153
  202. package/src/__tests__/audit-real-live.test.ts +0 -128
  203. package/src/__tests__/audit-reconstruct.test.ts +0 -353
  204. package/src/__tests__/audit-report.test.ts +0 -222
  205. package/src/__tests__/audit-roundtrip.test.ts +0 -464
  206. package/src/__tests__/audit-temporal.test.ts +0 -446
  207. package/src/__tests__/audit-tiers.test.ts +0 -343
  208. package/src/__tests__/audit-witness-real.test.ts +0 -92
  209. package/src/__tests__/audit-witness.test.ts +0 -345
  210. package/src/__tests__/canonical.test.ts +0 -253
  211. package/src/__tests__/chain-hash-anchor.test.ts +0 -67
  212. package/src/__tests__/commit-service.integration.test.ts +0 -117
  213. package/src/__tests__/constructor.test.ts +0 -340
  214. package/src/__tests__/declared-proof-fixture.ts +0 -65
  215. package/src/__tests__/fuse-inline.test.ts +0 -211
  216. package/src/__tests__/fuse-placement.test.ts +0 -32
  217. package/src/__tests__/fuse-roundtrip.test.ts +0 -243
  218. package/src/__tests__/fuse-sdk.test.ts +0 -142
  219. package/src/__tests__/fuse-set-sdk.test.ts +0 -852
  220. package/src/__tests__/fuse-set.test.ts +0 -1027
  221. package/src/__tests__/fuse-set2.test.ts +0 -343
  222. package/src/__tests__/fuse-vectors.test.ts +0 -218
  223. package/src/__tests__/fuse-verify.test.ts +0 -278
  224. package/src/__tests__/hello-world.test.ts +0 -10
  225. package/src/__tests__/mock-proof-fixture.ts +0 -36
  226. package/src/__tests__/policy-enforcement.test.ts +0 -226
  227. package/src/__tests__/proof-hash-regression.test.ts +0 -107
  228. package/src/__tests__/proof-hash.test.ts +0 -129
  229. package/src/__tests__/proof-integrity.test.ts +0 -576
  230. package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
  231. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
  232. package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
  233. package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
  234. package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
  235. package/src/__tests__/real-fixtures/successor-112760.json +0 -38
  236. package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
  237. package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
  238. package/src/__tests__/realistic-proof-fixture.ts +0 -57
  239. package/src/__tests__/verifier.test.ts +0 -624
@@ -1,771 +0,0 @@
1
- // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
- /**
3
- * Property-based topology tests for the audit pipeline (Phase 4e).
4
- *
5
- * A seeded deterministic PRNG (mulberry32, fixed literal seeds below, no
6
- * Date.now, no unseeded Math.random) drives a synthetic topology generator
7
- * that produces protocol-correct chains through the REAL signing path:
8
- * real Ed25519 signatures over the canonical SignedBody, real
9
- * computeProofHash links, and the G2 two-position slot/commit counter
10
- * pattern (slot 1/commit 2, slot 3/commit 4, ...). A mutation engine then
11
- * injects each anomaly class deliberately, each class in its own isolated
12
- * partition (fresh key, fresh epochId) so expected codes compose by
13
- * union.
14
- *
15
- * Assertions:
16
- * - each class in isolation produces exactly its stable codes plus the
17
- * class-specific structure (components, divergences, lineage edges);
18
- * - the property loop composes a random subset of classes per seed and
19
- * asserts EXACTLY the injected anomaly codes appear: no missing
20
- * detections, no spurious extras (control chains contribute zero);
21
- * - determinism: regenerating the same seed into a fresh directory
22
- * yields a deep-equal JSON report modulo runMetadata.
23
- *
24
- * No verifier semantics are bypassed anywhere: every proof that must be
25
- * valid is signed for real, and deliberately absent proofs are simply not
26
- * written to the bundle.
27
- */
28
- import { describe, it, after } from "node:test";
29
- import * as assert from "node:assert/strict";
30
- import { rm } from "node:fs/promises";
31
- import { sha256 } from "@noble/hashes/sha256";
32
- import { computeProofHash } from "@mikeargento/bitgraph-verify";
33
- import { buildJsonReport, runAudit } from "@mikeargento/bitgraph-audit";
34
- import { b64, makeCounterChain, makeEpochLinkProof, makeKey, makeTempDir, proofJson, signBody, utf8, writeBundleDir, healthyPairs, } from "./audit-fixtures.js";
35
- // ---------------------------------------------------------------------------
36
- // Seeded deterministic PRNG (mulberry32). Fixed seeds only; runs are
37
- // reproducible byte for byte.
38
- // ---------------------------------------------------------------------------
39
- function mulberry32(seed) {
40
- let a = seed >>> 0;
41
- return () => {
42
- a = (a + 0x6d2b79f5) >>> 0;
43
- let t = a;
44
- t = Math.imul(t ^ (t >>> 15), t | 1);
45
- t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
46
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
47
- };
48
- }
49
- function randomSource(rng) {
50
- return (byteLength) => {
51
- const out = new Uint8Array(byteLength);
52
- for (let i = 0; i < byteLength; i++)
53
- out[i] = Math.floor(rng() * 256);
54
- return out;
55
- };
56
- }
57
- function randInt(rng, min, max) {
58
- return min + Math.floor(rng() * (max - min + 1));
59
- }
60
- /** Fixed seeds for the property loop. Chosen once; never derived from the clock. */
61
- const PROPERTY_SEEDS = [
62
- 0x00000001, 0x1bad5eed, 0x2c0ffee5, 0x3d0d0bee, 0x4e1e5701, 0x5a5a5a5a,
63
- 0x6b17c0de, 0x7f00ba75, 0x8badf00d, 0x9e3779b9, 0xa11ce001, 0xb0bca742,
64
- ];
65
- /** Seed used by the isolation tests and the determinism test. */
66
- const ISOLATION_SEED = 0xf1c5e0e5;
67
- function makeContext(seed) {
68
- const rng = mulberry32(seed);
69
- let scenarioIndex = 0;
70
- return {
71
- rng,
72
- random: randomSource(rng),
73
- files: {},
74
- expectedCodes: [],
75
- expectedObserved: 0,
76
- expectedUnsupported: 0,
77
- expectedChainless: 0,
78
- nextPrefix: (className) => `s${String(scenarioIndex++).padStart(2, "0")}-${className}`,
79
- };
80
- }
81
- function addChainFiles(ctx, prefix, proofs) {
82
- for (let i = 0; i < proofs.length; i++) {
83
- ctx.files[`${prefix}/proof-${i}.json`] = proofJson(proofs[i].proof);
84
- }
85
- ctx.expectedObserved += proofs.length;
86
- }
87
- /** Random chainId variant: the named production chain or the enclave default. */
88
- function pickChainId(ctx) {
89
- return ctx.rng() < 0.5 ? "bitgraph:main" : undefined;
90
- }
91
- /** Continuous healthy chain: the control. Contributes zero codes. */
92
- const buildControl = async (ctx) => {
93
- const prefix = ctx.nextPrefix("control");
94
- const length = randInt(ctx.rng, 3, 6);
95
- const chainId = pickChainId(ctx);
96
- const chain = await makeCounterChain({
97
- epochId: `${prefix}-epoch`,
98
- pairs: healthyPairs(length),
99
- ...(chainId !== undefined ? { chainId } : {}),
100
- payloadPrefix: prefix,
101
- random: ctx.random,
102
- });
103
- addChainFiles(ctx, prefix, chain.proofs);
104
- };
105
- /** Removed proof: its slot+commit positions unexplained plus a chain break. */
106
- const buildRemovedProof = async (ctx) => {
107
- const prefix = ctx.nextPrefix("removed");
108
- const length = randInt(ctx.rng, 3, 6);
109
- const dropIndex = randInt(ctx.rng, 1, length - 2); // never genesis, never tail
110
- const chain = await makeCounterChain({
111
- epochId: `${prefix}-epoch`,
112
- pairs: healthyPairs(length),
113
- payloadPrefix: prefix,
114
- random: ctx.random,
115
- });
116
- for (let i = 0; i < chain.proofs.length; i++) {
117
- if (i === dropIndex)
118
- continue; // deliberately absent from the bundle
119
- ctx.files[`${prefix}/proof-${i}.json`] = proofJson(chain.proofs[i].proof);
120
- }
121
- ctx.expectedObserved += length - 1;
122
- ctx.expectedCodes.push("unexplained-counter-positions", "chain-break-missing");
123
- };
124
- /** Two proofs signed for the same commit counter (distinct slots, no shared prev). */
125
- const buildCounterCollision = async (ctx) => {
126
- const prefix = ctx.nextPrefix("counter-collision");
127
- const length = randInt(ctx.rng, 3, 5);
128
- const chain = await makeCounterChain({
129
- epochId: `${prefix}-epoch`,
130
- pairs: healthyPairs(length),
131
- payloadPrefix: prefix,
132
- random: ctx.random,
133
- });
134
- addChainFiles(ctx, prefix, chain.proofs);
135
- const collideWith = chain.proofs[1].proof.commit.counter; // "4"
136
- const tail = chain.proofs[length - 1];
137
- // The collider reuses only the commit counter and carries no slotCounter
138
- // of its own, so this scenario stays a pure counter collision: it never
139
- // reuses another proof's slot (slot-collision) and never reuses a commit
140
- // position as its own slot (cross-kind-position-reuse), both of which are
141
- // exercised by their own dedicated scenarios.
142
- const extra = await makeCounterChain({
143
- epochId: `${prefix}-epoch`,
144
- key: chain.key,
145
- pairs: [{ commit: collideWith }],
146
- payloadPrefix: `${prefix}-collider`,
147
- prevB64OfFirst: tail.chainHash,
148
- random: ctx.random,
149
- });
150
- ctx.files[`${prefix}/collider.json`] = proofJson(extra.proofs[0].proof);
151
- ctx.expectedObserved += 1;
152
- ctx.expectedCodes.push("counter-collision");
153
- };
154
- /** Two proofs referencing the same slot counter. */
155
- const buildSlotCollision = async (ctx) => {
156
- const prefix = ctx.nextPrefix("slot-collision");
157
- const length = randInt(ctx.rng, 3, 5);
158
- const chain = await makeCounterChain({
159
- epochId: `${prefix}-epoch`,
160
- pairs: healthyPairs(length),
161
- payloadPrefix: prefix,
162
- random: ctx.random,
163
- });
164
- addChainFiles(ctx, prefix, chain.proofs);
165
- const collideWith = chain.proofs[1].proof.commit.slotCounter;
166
- const tail = chain.proofs[length - 1];
167
- const extra = await makeCounterChain({
168
- epochId: `${prefix}-epoch`,
169
- key: chain.key,
170
- pairs: [{ slot: collideWith, commit: String(2 * length + 1) }],
171
- payloadPrefix: `${prefix}-collider`,
172
- prevB64OfFirst: tail.chainHash,
173
- random: ctx.random,
174
- });
175
- ctx.files[`${prefix}/collider.json`] = proofJson(extra.proofs[0].proof);
176
- ctx.expectedObserved += 1;
177
- ctx.expectedCodes.push("slot-collision");
178
- };
179
- /** Predecessor-reuse fork: two valid successors consume the same tail. */
180
- const buildPredecessorReuse = async (ctx) => {
181
- const prefix = ctx.nextPrefix("fork");
182
- const length = randInt(ctx.rng, 2, 4);
183
- const chain = await makeCounterChain({
184
- epochId: `${prefix}-epoch`,
185
- pairs: healthyPairs(length),
186
- payloadPrefix: prefix,
187
- random: ctx.random,
188
- });
189
- addChainFiles(ctx, prefix, chain.proofs);
190
- const tail = chain.proofs[length - 1];
191
- for (const [branch, offset] of [
192
- ["a", 0],
193
- ["b", 2],
194
- ]) {
195
- const child = await makeCounterChain({
196
- epochId: `${prefix}-epoch`,
197
- key: chain.key,
198
- pairs: [{ slot: String(2 * length + 1 + offset), commit: String(2 * length + 2 + offset) }],
199
- payloadPrefix: `${prefix}-branch-${branch}`,
200
- prevB64OfFirst: tail.chainHash,
201
- random: ctx.random,
202
- });
203
- ctx.files[`${prefix}/branch-${branch}.json`] = proofJson(child.proofs[0].proof);
204
- ctx.expectedObserved += 1;
205
- }
206
- ctx.expectedCodes.push("predecessor-reuse");
207
- };
208
- /** Dangling prevB64: well-formed 32-byte reference into unobserved history. */
209
- const buildMissingPredecessor = async (ctx) => {
210
- const prefix = ctx.nextPrefix("dangling-prev");
211
- const length = randInt(ctx.rng, 2, 4);
212
- const chain = await makeCounterChain({
213
- epochId: `${prefix}-epoch`,
214
- pairs: healthyPairs(length),
215
- payloadPrefix: prefix,
216
- random: ctx.random,
217
- });
218
- addChainFiles(ctx, prefix, chain.proofs);
219
- const stray = await makeCounterChain({
220
- epochId: `${prefix}-epoch`,
221
- key: chain.key,
222
- pairs: [{ slot: String(2 * length + 1), commit: String(2 * length + 2) }],
223
- payloadPrefix: `${prefix}-stray`,
224
- prevB64OfFirst: b64(ctx.random(32)), // no observed proof has this hash
225
- random: ctx.random,
226
- });
227
- ctx.files[`${prefix}/stray.json`] = proofJson(stray.proofs[0].proof);
228
- ctx.expectedObserved += 1;
229
- ctx.expectedCodes.push("chain-break-missing");
230
- };
231
- /** Malformed prevB64: not base64 of 32 bytes. The proof itself stays verifier-valid. */
232
- const buildMalformedPrev = async (ctx) => {
233
- const prefix = ctx.nextPrefix("malformed-prev");
234
- const length = randInt(ctx.rng, 2, 4);
235
- const chain = await makeCounterChain({
236
- epochId: `${prefix}-epoch`,
237
- pairs: healthyPairs(length),
238
- payloadPrefix: prefix,
239
- random: ctx.random,
240
- });
241
- addChainFiles(ctx, prefix, chain.proofs);
242
- const malformed = await makeCounterChain({
243
- epochId: `${prefix}-epoch`,
244
- key: chain.key,
245
- pairs: [{ slot: String(2 * length + 1), commit: String(2 * length + 2) }],
246
- payloadPrefix: `${prefix}-malformed`,
247
- prevB64OfFirst: "this-is-not-base64-of-32-bytes!",
248
- random: ctx.random,
249
- });
250
- ctx.files[`${prefix}/malformed.json`] = proofJson(malformed.proofs[0].proof);
251
- ctx.expectedObserved += 1;
252
- ctx.expectedCodes.push("chain-break-malformed");
253
- };
254
- /** Byte-identical duplicate proof file. Benign; still a stable code. */
255
- const buildExactDuplicate = async (ctx) => {
256
- const prefix = ctx.nextPrefix("exact-dup");
257
- const chain = await makeCounterChain({
258
- epochId: `${prefix}-epoch`,
259
- pairs: healthyPairs(2),
260
- payloadPrefix: prefix,
261
- random: ctx.random,
262
- });
263
- addChainFiles(ctx, prefix, chain.proofs);
264
- ctx.files[`${prefix}/copy-of-0.json`] = proofJson(chain.proofs[0].proof);
265
- ctx.expectedCodes.push("exact-duplicate");
266
- };
267
- /**
268
- * Semantic duplicate: the same proof re-serialized with a different key
269
- * order and an extra UNSIGNED metadata field, so the bytes differ but the
270
- * canonical identity matches. The builder verifies the computeProofHash
271
- * equality before relying on it (metadata sits outside the signed body).
272
- */
273
- const buildSemanticDuplicate = async (ctx) => {
274
- const prefix = ctx.nextPrefix("semantic-dup");
275
- const chain = await makeCounterChain({
276
- epochId: `${prefix}-epoch`,
277
- pairs: healthyPairs(2),
278
- payloadPrefix: prefix,
279
- random: ctx.random,
280
- });
281
- addChainFiles(ctx, prefix, chain.proofs);
282
- const original = chain.proofs[0].proof;
283
- const reserialized = {
284
- environment: original.environment,
285
- signer: original.signer,
286
- commit: original.commit,
287
- artifact: original.artifact,
288
- version: original.version,
289
- metadata: { note: "unsigned metadata added by a relay", relay: prefix },
290
- };
291
- const reserializedJson = JSON.stringify(reserialized);
292
- assert.notEqual(reserializedJson, proofJson(original), "bytes must differ");
293
- assert.equal(computeProofHash(JSON.parse(reserializedJson)), chain.proofs[0].proofHash, "re-serialization with reordered keys and unsigned metadata must keep the canonical identity");
294
- ctx.files[`${prefix}/reserialized-0.json`] = reserializedJson;
295
- ctx.expectedCodes.push("semantic-duplicate");
296
- };
297
- /** Two disconnected components in one partition: a second no-prevB64 genesis. */
298
- const buildDisconnectedChains = async (ctx) => {
299
- const prefix = ctx.nextPrefix("disconnected");
300
- const first = await makeCounterChain({
301
- epochId: `${prefix}-epoch`,
302
- pairs: healthyPairs(2),
303
- payloadPrefix: `${prefix}-one`,
304
- random: ctx.random,
305
- });
306
- addChainFiles(ctx, `${prefix}/one`, first.proofs);
307
- // Same key and epoch, later counters, no link back: a second genesis.
308
- const second = await makeCounterChain({
309
- epochId: `${prefix}-epoch`,
310
- key: first.key,
311
- pairs: [
312
- { slot: "5", commit: "6" },
313
- { slot: "7", commit: "8" },
314
- ],
315
- payloadPrefix: `${prefix}-two`,
316
- random: ctx.random,
317
- });
318
- addChainFiles(ctx, `${prefix}/two`, second.proofs);
319
- ctx.expectedCodes.push("multiple-genesis");
320
- };
321
- /** Valid epochLink transition: prior epoch terminal consumed by a new epoch. Zero codes. */
322
- const buildValidEpochLink = async (ctx) => {
323
- const prefix = ctx.nextPrefix("epochlink-valid");
324
- const length = randInt(ctx.rng, 2, 3);
325
- // One measurement per epoch: an epochId is boot-scoped, so the genesis
326
- // and its continuation declare the same measurement (a mix would
327
- // honestly flag mid-epoch-measurement-change).
328
- const measurementB = `${prefix}-measurement-b`;
329
- const prior = await makeCounterChain({
330
- epochId: `${prefix}-epoch-a`,
331
- pairs: healthyPairs(length),
332
- measurement: `${prefix}-measurement-a`,
333
- payloadPrefix: `${prefix}-a`,
334
- random: ctx.random,
335
- });
336
- addChainFiles(ctx, `${prefix}/a`, prior.proofs);
337
- const terminal = prior.proofs[length - 1];
338
- const genesis = await makeEpochLinkProof({
339
- prevEpochId: `${prefix}-epoch-a`,
340
- prevCounter: terminal.proof.commit.counter,
341
- prevProofHashB64: terminal.chainHash,
342
- toEpochId: `${prefix}-epoch-b`,
343
- prevPublicKeyB64: prior.key.publicKeyB64,
344
- counter: "2",
345
- slotCounter: "1",
346
- measurement: measurementB,
347
- payload: `${prefix}-b-genesis`,
348
- random: ctx.random,
349
- });
350
- ctx.files[`${prefix}/b-genesis.json`] = proofJson(genesis.proof);
351
- const continuation = await makeCounterChain({
352
- epochId: `${prefix}-epoch-b`,
353
- key: genesis.key,
354
- pairs: [{ slot: "3", commit: "4" }],
355
- measurement: measurementB,
356
- payloadPrefix: `${prefix}-b`,
357
- prevB64OfFirst: genesis.chainHash,
358
- random: ctx.random,
359
- });
360
- ctx.files[`${prefix}/b-1.json`] = proofJson(continuation.proofs[0].proof);
361
- ctx.expectedObserved += 2;
362
- };
363
- /** Broken epochLink: dangling reference to an unobserved epoch and terminal. */
364
- const buildDanglingEpochLink = async (ctx) => {
365
- const prefix = ctx.nextPrefix("epochlink-dangling");
366
- const genesis = await makeEpochLinkProof({
367
- prevEpochId: `${prefix}-unobserved-epoch`,
368
- prevCounter: "88",
369
- prevProofHashB64: b64(ctx.random(32)),
370
- toEpochId: `${prefix}-epoch`,
371
- counter: "2",
372
- slotCounter: "1",
373
- payload: `${prefix}-genesis`,
374
- random: ctx.random,
375
- });
376
- ctx.files[`${prefix}/genesis.json`] = proofJson(genesis.proof);
377
- ctx.expectedObserved += 1;
378
- ctx.expectedCodes.push("epochlink-dangling");
379
- };
380
- /** Multiple independent lineages: two signers, two epochs. Zero codes, never merged. */
381
- const buildMultipleLineages = async (ctx) => {
382
- const prefix = ctx.nextPrefix("lineages");
383
- for (const which of ["x", "y"]) {
384
- const chain = await makeCounterChain({
385
- epochId: `${prefix}-epoch-${which}`,
386
- pairs: healthyPairs(randInt(ctx.rng, 2, 3)),
387
- payloadPrefix: `${prefix}-${which}`,
388
- random: ctx.random,
389
- });
390
- addChainFiles(ctx, `${prefix}/${which}`, chain.proofs);
391
- }
392
- };
393
- /** Mid-epoch measurement transition: one signer, one epoch, linked chain, two measurements. */
394
- const buildMeasurementChange = async (ctx) => {
395
- const prefix = ctx.nextPrefix("measurement-change");
396
- const first = await makeCounterChain({
397
- epochId: `${prefix}-epoch`,
398
- pairs: healthyPairs(2),
399
- measurement: `${prefix}-measurement-one`,
400
- payloadPrefix: `${prefix}-one`,
401
- random: ctx.random,
402
- });
403
- addChainFiles(ctx, `${prefix}/one`, first.proofs);
404
- const second = await makeCounterChain({
405
- epochId: `${prefix}-epoch`,
406
- key: first.key,
407
- pairs: [{ slot: "5", commit: "6" }],
408
- measurement: `${prefix}-measurement-two`,
409
- payloadPrefix: `${prefix}-two`,
410
- prevB64OfFirst: first.proofs[1].chainHash,
411
- random: ctx.random,
412
- });
413
- ctx.files[`${prefix}/two.json`] = proofJson(second.proofs[0].proof);
414
- ctx.expectedObserved += 1;
415
- ctx.expectedCodes.push("mid-epoch-measurement-change");
416
- };
417
- /** Illegal mid-epoch signer change: two distinct keys under one epochId. */
418
- const buildSignerChange = async (ctx) => {
419
- const prefix = ctx.nextPrefix("signer-change");
420
- const epochId = `${prefix}-epoch`;
421
- const first = await makeCounterChain({
422
- epochId,
423
- pairs: healthyPairs(2),
424
- payloadPrefix: `${prefix}-one`,
425
- random: ctx.random,
426
- });
427
- addChainFiles(ctx, `${prefix}/one`, first.proofs);
428
- const secondKey = await makeKey(ctx.random);
429
- const second = await makeCounterChain({
430
- epochId,
431
- key: secondKey,
432
- pairs: [
433
- { slot: "5", commit: "6" },
434
- { slot: "7", commit: "8" },
435
- ],
436
- payloadPrefix: `${prefix}-two`,
437
- random: ctx.random,
438
- });
439
- addChainFiles(ctx, `${prefix}/two`, second.proofs);
440
- ctx.expectedCodes.push("mid-epoch-signer-change");
441
- };
442
- /** Chainless proofs: no counter, no epochId. Observed-but-unchained, never an anomaly. */
443
- const buildChainless = async (ctx) => {
444
- const prefix = ctx.nextPrefix("chainless");
445
- const count = randInt(ctx.rng, 1, 2);
446
- const key = await makeKey(ctx.random);
447
- for (let i = 0; i < count; i++) {
448
- const bytes = utf8(`${prefix}-payload-${i}`);
449
- const proof = await signBody(key, { hashAlg: "sha256", digestB64: b64(sha256(bytes)) }, { nonceB64: b64(ctx.random(16)) }, `${prefix}-measurement`);
450
- ctx.files[`${prefix}/proof-${i}.json`] = proofJson(proof);
451
- }
452
- ctx.expectedObserved += count;
453
- ctx.expectedChainless += count;
454
- };
455
- /** occ/1 reject: pre-release beta data, rejected at ingest per the version policy. */
456
- const buildOccReject = async (ctx) => {
457
- const prefix = ctx.nextPrefix("occ1");
458
- ctx.files[`${prefix}/legacy.json`] = JSON.stringify({
459
- version: "occ/1",
460
- artifact: { hashAlg: "sha256", digestB64: b64(ctx.random(32)) },
461
- commit: { nonceB64: b64(ctx.random(16)), counter: "7" },
462
- signer: { publicKeyB64: b64(ctx.random(32)), signatureB64: b64(ctx.random(64)) },
463
- });
464
- ctx.expectedUnsupported += 1;
465
- ctx.expectedCodes.push("unsupported-version");
466
- };
467
- const MUTATIONS = [
468
- ["removed-proof", buildRemovedProof],
469
- ["counter-collision", buildCounterCollision],
470
- ["slot-collision", buildSlotCollision],
471
- ["predecessor-reuse", buildPredecessorReuse],
472
- ["missing-predecessor", buildMissingPredecessor],
473
- ["malformed-prev", buildMalformedPrev],
474
- ["exact-duplicate", buildExactDuplicate],
475
- ["semantic-duplicate", buildSemanticDuplicate],
476
- ["disconnected-chains", buildDisconnectedChains],
477
- ["epochlink-valid", buildValidEpochLink],
478
- ["epochlink-dangling", buildDanglingEpochLink],
479
- ["multiple-lineages", buildMultipleLineages],
480
- ["measurement-change", buildMeasurementChange],
481
- ["signer-change", buildSignerChange],
482
- ["chainless", buildChainless],
483
- ["occ1-reject", buildOccReject],
484
- ];
485
- // ---------------------------------------------------------------------------
486
- // Shared runner
487
- // ---------------------------------------------------------------------------
488
- const tempDirs = [];
489
- after(async () => {
490
- await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })));
491
- });
492
- async function buildAndAudit(ctx) {
493
- const dir = await makeTempDir("bitgraph-audit-property-");
494
- tempDirs.push(dir);
495
- await writeBundleDir(dir, ctx.files);
496
- const result = await runAudit(dir);
497
- return { dir, result };
498
- }
499
- function assertExactCodes(result, ctx, label) {
500
- const report = buildJsonReport(result);
501
- const actual = report.anomalies.map((a) => a.code).sort();
502
- const expected = [...ctx.expectedCodes].sort();
503
- assert.deepEqual(actual, expected, `${label}: exactly the injected anomaly codes appear`);
504
- assert.equal(result.ingest.counts.observed, ctx.expectedObserved, `${label}: observed count`);
505
- assert.equal(result.ingest.counts.unsupportedVersion, ctx.expectedUnsupported, `${label}: unsupported-version count`);
506
- assert.equal(result.verification.chainless, ctx.expectedChainless, `${label}: chainless count`);
507
- // Every synthetic proof is signed through the real path with no artifact
508
- // bytes bundled: nothing may fail verification, nothing may be "verified".
509
- assert.equal(result.verification.failed, 0, `${label}: no verification failures`);
510
- assert.equal(result.verification.verified, 0, `${label}: no artifact bytes, no full tier`);
511
- assert.equal(result.verification.artifactUnavailable, ctx.expectedObserved, `${label}: all proofs at the integrity tier`);
512
- }
513
- function findPartition(result, epochId) {
514
- const report = buildJsonReport(result);
515
- return report.partitions.find((p) => p.key.epochId === epochId);
516
- }
517
- // ---------------------------------------------------------------------------
518
- // Isolation: each anomaly class alone produces exactly its codes plus the
519
- // class-specific structure.
520
- // ---------------------------------------------------------------------------
521
- describe("audit property tests: anomaly classes in isolation", () => {
522
- it("control: a continuous healthy chain produces zero anomaly codes", async () => {
523
- const ctx = makeContext(ISOLATION_SEED);
524
- await buildControl(ctx);
525
- const { result } = await buildAndAudit(ctx);
526
- assertExactCodes(result, ctx, "control");
527
- assert.equal(result.reconstruction.partitions.length, 1);
528
- assert.equal(result.reconstruction.partitions[0]?.components.length, 1);
529
- assert.equal(buildJsonReport(result).summary.chainIntact, true);
530
- });
531
- it("removed proof: gap covers exactly the dropped slot and commit positions, plus a chain break", async () => {
532
- const ctx = makeContext(ISOLATION_SEED + 1);
533
- // Deterministic variant: length 4, drop index 1 (slot 3 / commit 4).
534
- const prefix = ctx.nextPrefix("removed-fixed");
535
- const chain = await makeCounterChain({
536
- epochId: `${prefix}-epoch`,
537
- pairs: healthyPairs(4),
538
- payloadPrefix: prefix,
539
- random: ctx.random,
540
- });
541
- for (let i = 0; i < 4; i++) {
542
- if (i === 1)
543
- continue;
544
- ctx.files[`${prefix}/proof-${i}.json`] = proofJson(chain.proofs[i].proof);
545
- }
546
- ctx.expectedObserved += 3;
547
- ctx.expectedCodes.push("unexplained-counter-positions", "chain-break-missing");
548
- const { result } = await buildAndAudit(ctx);
549
- assertExactCodes(result, ctx, "removed");
550
- const gap = result.anomalies.anomalies.find((a) => a.code === "unexplained-counter-positions");
551
- assert.deepEqual((gap?.details).positions, ["3", "4"]);
552
- const brk = result.anomalies.anomalies.find((a) => a.code === "chain-break-missing");
553
- assert.deepEqual(brk?.proofHashes, [chain.proofs[2].proofHash]);
554
- assert.equal((brk?.details).prevB64, chain.proofs[1].chainHash);
555
- // The removal splits the partition into two observed components.
556
- assert.equal(findPartition(result, `${prefix}-epoch`)?.components.length, 2);
557
- });
558
- it("counter collision: two valid proofs on one commit counter, divergence preserved, no winner", async () => {
559
- const ctx = makeContext(ISOLATION_SEED + 2);
560
- await buildCounterCollision(ctx);
561
- const { result } = await buildAndAudit(ctx);
562
- assertExactCodes(result, ctx, "counter-collision");
563
- assert.equal(result.anomalies.divergences.length, 1);
564
- const divergence = result.anomalies.divergences[0];
565
- assert.equal(divergence.kind, "counter-collision");
566
- assert.equal(divergence.parties.length, 2);
567
- assert.equal(divergence.invalidContext.length, 0);
568
- assert.ok(divergence.explanation.includes("does not choose"));
569
- });
570
- it("slot collision: two valid proofs referencing one slot counter", async () => {
571
- const ctx = makeContext(ISOLATION_SEED + 3);
572
- await buildSlotCollision(ctx);
573
- const { result } = await buildAndAudit(ctx);
574
- assertExactCodes(result, ctx, "slot-collision");
575
- assert.equal(result.anomalies.divergences.length, 1);
576
- assert.equal(result.anomalies.divergences[0]?.kind, "slot-collision");
577
- });
578
- it("predecessor reuse: a fork with both branches preserved", async () => {
579
- const ctx = makeContext(ISOLATION_SEED + 4);
580
- await buildPredecessorReuse(ctx);
581
- const { result } = await buildAndAudit(ctx);
582
- assertExactCodes(result, ctx, "predecessor-reuse");
583
- assert.equal(result.anomalies.divergences.length, 1);
584
- const divergence = result.anomalies.divergences[0];
585
- assert.equal(divergence.kind, "predecessor-reuse");
586
- assert.equal(divergence.parties.length, 2);
587
- // Both branches verifier-valid at the integrity tier.
588
- for (const party of divergence.parties) {
589
- assert.equal(party.verificationStatus, "artifact-unavailable");
590
- }
591
- });
592
- it("missing predecessor: dangling prevB64 is a chain break into unobserved history", async () => {
593
- const ctx = makeContext(ISOLATION_SEED + 5);
594
- await buildMissingPredecessor(ctx);
595
- const { result } = await buildAndAudit(ctx);
596
- assertExactCodes(result, ctx, "missing-predecessor");
597
- const brk = result.anomalies.anomalies.find((a) => a.code === "chain-break-missing");
598
- assert.ok(brk?.message.includes("does not, by itself, establish"));
599
- });
600
- it("malformed prevB64: unusable link flagged; the proof itself stays verifier-valid", async () => {
601
- const ctx = makeContext(ISOLATION_SEED + 6);
602
- await buildMalformedPrev(ctx);
603
- const { result } = await buildAndAudit(ctx);
604
- assertExactCodes(result, ctx, "malformed-prev");
605
- const anomaly = result.anomalies.anomalies.find((a) => a.code === "chain-break-malformed");
606
- const flagged = result.ingest.proofs.find((p) => p.proofHash === anomaly?.proofHashes[0]);
607
- // prevB64 is inside the signed body but the canonical verifier does not
608
- // require it to be base64; topology and verification stay separate.
609
- assert.equal(flagged?.verification?.status, "artifact-unavailable");
610
- });
611
- it("exact duplicate: byte-identical copy collapses to one observed proof", async () => {
612
- const ctx = makeContext(ISOLATION_SEED + 7);
613
- await buildExactDuplicate(ctx);
614
- const { result } = await buildAndAudit(ctx);
615
- assertExactCodes(result, ctx, "exact-duplicate");
616
- assert.equal(result.ingest.counts.exactDuplicates, 1);
617
- assert.equal(result.ingest.counts.semanticDuplicates, 0);
618
- });
619
- it("semantic duplicate: reordered keys plus unsigned metadata keep the canonical identity", async () => {
620
- const ctx = makeContext(ISOLATION_SEED + 8);
621
- await buildSemanticDuplicate(ctx);
622
- const { result } = await buildAndAudit(ctx);
623
- assertExactCodes(result, ctx, "semantic-duplicate");
624
- assert.equal(result.ingest.counts.exactDuplicates, 0);
625
- assert.equal(result.ingest.counts.semanticDuplicates, 1);
626
- // The two byte encodings collapsed into one observed proof with two sources.
627
- const withTwoSources = result.ingest.proofs.filter((p) => p.sources.length === 2);
628
- assert.equal(withTwoSources.length, 1);
629
- });
630
- it("disconnected chains: two components in one partition via a second genesis", async () => {
631
- const ctx = makeContext(ISOLATION_SEED + 9);
632
- await buildDisconnectedChains(ctx);
633
- const { result } = await buildAndAudit(ctx);
634
- assertExactCodes(result, ctx, "disconnected");
635
- assert.equal(result.reconstruction.partitions.length, 1);
636
- assert.equal(result.reconstruction.partitions[0]?.components.length, 2);
637
- // Both genesis proofs are valid: a multiple-genesis divergence.
638
- assert.equal(result.anomalies.divergences.length, 1);
639
- assert.equal(result.anomalies.divergences[0]?.kind, "multiple-genesis");
640
- });
641
- it("valid epochLink transition: zero codes, hard lineage edge, ordered epochs", async () => {
642
- const ctx = makeContext(ISOLATION_SEED + 10);
643
- await buildValidEpochLink(ctx);
644
- const { result } = await buildAndAudit(ctx);
645
- assertExactCodes(result, ctx, "epochlink-valid");
646
- const edges = result.reconstruction.epochRelationships.edges;
647
- assert.equal(edges.length, 1);
648
- assert.equal(edges[0]?.resolution, "matched");
649
- assert.equal(edges[0]?.hardEdge, true);
650
- assert.equal(edges[0]?.metadataConsistent, true);
651
- const pairs = result.reconstruction.epochRelationships.orderedPairs;
652
- assert.equal(pairs.length, 1);
653
- assert.ok(pairs[0]?.beforeEpochId.endsWith("epoch-a"));
654
- assert.ok(pairs[0]?.afterEpochId.endsWith("epoch-b"));
655
- for (const epoch of result.reconstruction.epochRelationships.epochs) {
656
- assert.equal(epoch.ordering, "linked");
657
- }
658
- });
659
- it("broken epochLink: dangling lineage claim flagged, no ordering asserted", async () => {
660
- const ctx = makeContext(ISOLATION_SEED + 11);
661
- await buildDanglingEpochLink(ctx);
662
- const { result } = await buildAndAudit(ctx);
663
- assertExactCodes(result, ctx, "epochlink-dangling");
664
- const edges = result.reconstruction.epochRelationships.edges;
665
- assert.equal(edges.length, 1);
666
- assert.equal(edges[0]?.resolution, "dangling");
667
- assert.equal(edges[0]?.hardEdge, false);
668
- assert.equal(result.reconstruction.epochRelationships.orderedPairs.length, 0);
669
- });
670
- it("multiple lineages: two signers never merge and never anomalize", async () => {
671
- const ctx = makeContext(ISOLATION_SEED + 12);
672
- await buildMultipleLineages(ctx);
673
- const { result } = await buildAndAudit(ctx);
674
- assertExactCodes(result, ctx, "lineages");
675
- assert.equal(result.reconstruction.partitions.length, 2);
676
- const keys = new Set(result.reconstruction.partitions.map((p) => p.key.publicKeyB64));
677
- assert.equal(keys.size, 2);
678
- // No ordering evidence between the epochs: concurrent-or-unordered.
679
- assert.equal(buildJsonReport(result).epochRelationships.unorderedPairs.length, 1);
680
- });
681
- it("mid-epoch measurement transition: flagged with both measurements listed", async () => {
682
- const ctx = makeContext(ISOLATION_SEED + 13);
683
- await buildMeasurementChange(ctx);
684
- const { result } = await buildAndAudit(ctx);
685
- assertExactCodes(result, ctx, "measurement-change");
686
- const anomaly = result.authorities.anomalies.find((a) => a.code === "mid-epoch-measurement-change");
687
- assert.ok(anomaly !== undefined);
688
- const details = anomaly.details;
689
- assert.equal(details.measurements.length, 2);
690
- // One signer, one linked chain: still a single component.
691
- assert.equal(result.reconstruction.partitions.length, 1);
692
- assert.equal(result.reconstruction.partitions[0]?.components.length, 1);
693
- // Two authority groups (measurement is a grouping facet).
694
- assert.equal(result.authorities.groups.length, 2);
695
- });
696
- it("illegal mid-epoch signer change: flagged with both keys listed", async () => {
697
- const ctx = makeContext(ISOLATION_SEED + 14);
698
- await buildSignerChange(ctx);
699
- const { result } = await buildAndAudit(ctx);
700
- assertExactCodes(result, ctx, "signer-change");
701
- const anomaly = result.authorities.anomalies.find((a) => a.code === "mid-epoch-signer-change");
702
- assert.ok(anomaly !== undefined);
703
- const details = anomaly.details;
704
- assert.equal(details.publicKeysB64.length, 2);
705
- // Two partitions: signer lineages are never merged.
706
- assert.equal(result.reconstruction.partitions.length, 2);
707
- });
708
- it("chainless proofs: observed-but-unchained, reported separately, never an anomaly", async () => {
709
- const ctx = makeContext(ISOLATION_SEED + 15);
710
- await buildChainless(ctx);
711
- const { result } = await buildAndAudit(ctx);
712
- assertExactCodes(result, ctx, "chainless");
713
- assert.equal(result.reconstruction.partitions.length, 0);
714
- assert.equal(result.reconstruction.unchainedProofHashes.length, ctx.expectedChainless);
715
- });
716
- it("occ/1 reject: unsupported-version at ingest, excluded from observation and analysis", async () => {
717
- const ctx = makeContext(ISOLATION_SEED + 16);
718
- await buildOccReject(ctx);
719
- await buildControl(ctx);
720
- const { result } = await buildAndAudit(ctx);
721
- assertExactCodes(result, ctx, "occ1");
722
- assert.equal(result.ingest.unsupportedVersions.length, 1);
723
- assert.equal(result.ingest.unsupportedVersions[0]?.version, "occ/1");
724
- assert.ok(result.ingest.unsupportedVersions[0]?.path.includes("legacy.json"));
725
- // Rejected inputs are not observed objects and join no partition.
726
- assert.equal(result.ingest.counts.observed, ctx.expectedObserved);
727
- });
728
- });
729
- // ---------------------------------------------------------------------------
730
- // Property loop: random topologies across fixed seeds.
731
- // ---------------------------------------------------------------------------
732
- describe("audit property tests: random topologies across fixed seeds", () => {
733
- for (const seed of PROPERTY_SEEDS) {
734
- it(`seed 0x${seed.toString(16)}: exactly the injected anomaly codes appear`, async () => {
735
- const ctx = makeContext(seed);
736
- // Always a control chain; each mutation class joins with p = 0.5.
737
- // Selection draws happen BEFORE any builder runs so the include/skip
738
- // pattern is a pure function of the seed.
739
- const included = MUTATIONS.map(() => ctx.rng() < 0.5);
740
- await buildControl(ctx);
741
- for (let i = 0; i < MUTATIONS.length; i++) {
742
- if (included[i] === true)
743
- await MUTATIONS[i][1](ctx);
744
- }
745
- const { result } = await buildAndAudit(ctx);
746
- const label = `seed 0x${seed.toString(16)} [${MUTATIONS.filter((_, i) => included[i]).map(([n]) => n).join(", ")}]`;
747
- assertExactCodes(result, ctx, label);
748
- });
749
- }
750
- it("determinism: the same seed regenerated twice yields identical reports modulo runMetadata", async () => {
751
- const build = async () => {
752
- const ctx = makeContext(ISOLATION_SEED + 100);
753
- const included = MUTATIONS.map(() => ctx.rng() < 0.5);
754
- await buildControl(ctx);
755
- for (let i = 0; i < MUTATIONS.length; i++) {
756
- if (included[i] === true)
757
- await MUTATIONS[i][1](ctx);
758
- }
759
- const { result } = await buildAndAudit(ctx);
760
- return { report: buildJsonReport(result) };
761
- };
762
- const first = await build();
763
- const second = await build();
764
- const { runMetadata: metaA, ...restA } = first.report;
765
- const { runMetadata: metaB, ...restB } = second.report;
766
- assert.notEqual(metaA.bundlePath, metaB.bundlePath, "distinct temp dirs");
767
- assert.deepEqual(restA, restB, "reports deep-equal modulo runMetadata");
768
- assert.equal(JSON.stringify(restA), JSON.stringify(restB), "byte-equal serialization");
769
- });
770
- });
771
- //# sourceMappingURL=audit-property.test.js.map