@mikeargento/bitgraph 1.1.0 → 1.1.1

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 (144) 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__/mock-proof-fixture.d.ts +15 -0
  91. package/dist/__tests__/mock-proof-fixture.d.ts.map +1 -0
  92. package/dist/__tests__/mock-proof-fixture.js +34 -0
  93. package/dist/__tests__/mock-proof-fixture.js.map +1 -0
  94. package/dist/__tests__/proof-hash-regression.test.js +4 -43
  95. package/dist/__tests__/proof-hash-regression.test.js.map +1 -1
  96. package/dist/__tests__/proof-hash.test.js +3 -21
  97. package/dist/__tests__/proof-hash.test.js.map +1 -1
  98. package/dist/__tests__/proof-integrity.test.d.ts +2 -0
  99. package/dist/__tests__/proof-integrity.test.d.ts.map +1 -0
  100. package/dist/__tests__/proof-integrity.test.js +426 -0
  101. package/dist/__tests__/proof-integrity.test.js.map +1 -0
  102. package/dist/__tests__/realistic-proof-fixture.d.ts +56 -0
  103. package/dist/__tests__/realistic-proof-fixture.d.ts.map +1 -0
  104. package/dist/__tests__/realistic-proof-fixture.js +57 -0
  105. package/dist/__tests__/realistic-proof-fixture.js.map +1 -0
  106. package/dist/__tests__/verifier.test.js +74 -0
  107. package/dist/__tests__/verifier.test.js.map +1 -1
  108. package/package.json +10 -5
  109. package/src/__tests__/audit-anchors.test.ts +188 -0
  110. package/src/__tests__/audit-anomalies.test.ts +815 -0
  111. package/src/__tests__/audit-attestation.test.ts +529 -0
  112. package/src/__tests__/audit-bench-smoke.test.ts +108 -0
  113. package/src/__tests__/audit-boundary.test.ts +96 -0
  114. package/src/__tests__/audit-cli.test.ts +288 -0
  115. package/src/__tests__/audit-dependencies.test.ts +141 -0
  116. package/src/__tests__/audit-fixtures.ts +736 -0
  117. package/src/__tests__/audit-ingest.test.ts +575 -0
  118. package/src/__tests__/audit-memory-ingest.test.ts +140 -0
  119. package/src/__tests__/audit-property.test.ts +892 -0
  120. package/src/__tests__/audit-real-fixtures.test.ts +153 -0
  121. package/src/__tests__/audit-real-live.test.ts +128 -0
  122. package/src/__tests__/audit-reconstruct.test.ts +353 -0
  123. package/src/__tests__/audit-report.test.ts +222 -0
  124. package/src/__tests__/audit-roundtrip.test.ts +464 -0
  125. package/src/__tests__/audit-temporal.test.ts +446 -0
  126. package/src/__tests__/audit-tiers.test.ts +343 -0
  127. package/src/__tests__/audit-witness-real.test.ts +92 -0
  128. package/src/__tests__/audit-witness.test.ts +345 -0
  129. package/src/__tests__/chain-hash-anchor.test.ts +67 -0
  130. package/src/__tests__/declared-proof-fixture.ts +65 -0
  131. package/src/__tests__/mock-proof-fixture.ts +36 -0
  132. package/src/__tests__/proof-hash-regression.test.ts +4 -44
  133. package/src/__tests__/proof-hash.test.ts +3 -22
  134. package/src/__tests__/proof-integrity.test.ts +541 -0
  135. package/src/__tests__/real-fixtures/anchor-after-112762.json +47 -0
  136. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +47 -0
  137. package/src/__tests__/real-fixtures/live-proof-0.json +42 -0
  138. package/src/__tests__/real-fixtures/live-proof-1.json +43 -0
  139. package/src/__tests__/real-fixtures/live-proof-2.json +43 -0
  140. package/src/__tests__/real-fixtures/successor-112760.json +38 -0
  141. package/src/__tests__/real-fixtures/witness-25525146.json +6 -0
  142. package/src/__tests__/real-fixtures/witness-25525147.json +6 -0
  143. package/src/__tests__/realistic-proof-fixture.ts +57 -0
  144. package/src/__tests__/verifier.test.ts +83 -0
@@ -0,0 +1,736 @@
1
+ // Copyright (c) Mike Argento. All rights reserved. See LICENSE.
2
+
3
+ /**
4
+ * Shared fixture helpers for the bitgraph-audit test suites.
5
+ *
6
+ * Proofs that must be verifier-valid are built either through the root
7
+ * Constructor (real commit path) or by manually signing the exact
8
+ * canonical SignedBody layout with real Ed25519 keys, the same approach
9
+ * as src/__tests__/proof-integrity.test.ts. Verifier semantics are never
10
+ * bypassed.
11
+ *
12
+ * Also provides a minimal in-memory tar writer (ustar, PAX path records,
13
+ * GNU long-name entries) so container tests never shell out.
14
+ */
15
+
16
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
17
+ import { tmpdir } from "node:os";
18
+ import { dirname, join } from "node:path";
19
+ import { getPublicKeyAsync, signAsync } from "@noble/ed25519";
20
+ import { sha256 } from "@noble/hashes/sha256";
21
+ import { keccak_256 } from "@noble/hashes/sha3";
22
+ import { canonicalize, computeProofHash, computeChainHash } from "@mikeargento/bitgraph-verify";
23
+ import type { BitGraphProof } from "@mikeargento/bitgraph-verify";
24
+ import { Constructor } from "../constructor.js";
25
+ import type { HostCapabilities } from "../host.js";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Encoding helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export function b64(bytes: Uint8Array): string {
32
+ return Buffer.from(bytes).toString("base64");
33
+ }
34
+
35
+ export function utf8(text: string): Uint8Array {
36
+ return new TextEncoder().encode(text);
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Proof fixtures
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Source of random bytes for fixture generation. Defaults everywhere to
45
+ * crypto.getRandomValues; property-based suites pass a seeded deterministic
46
+ * source so the same seed always regenerates byte-identical fixtures.
47
+ */
48
+ export type RandomSource = (byteLength: number) => Uint8Array;
49
+
50
+ const defaultRandom: RandomSource = (n) => crypto.getRandomValues(new Uint8Array(n));
51
+
52
+ export interface ManualKey {
53
+ privateKey: Uint8Array;
54
+ publicKeyB64: string;
55
+ }
56
+
57
+ export async function makeKey(random?: RandomSource): Promise<ManualKey> {
58
+ const privateKey = (random ?? defaultRandom)(32);
59
+ const publicKeyB64 = b64(await getPublicKeyAsync(privateKey));
60
+ return { privateKey, publicKeyB64 };
61
+ }
62
+
63
+ /**
64
+ * Manually build and sign a proof over the exact canonical SignedBody the
65
+ * verifier reconstructs. The commit object may carry extra fields (for
66
+ * example the live enclave's undeclared chainId); because it is part of
67
+ * the signed body, the signature covers them.
68
+ *
69
+ * Optional extras follow the canonical SignedBody rules: attribution is
70
+ * included in the signed body when provided; attestationFormat is
71
+ * included when an attestation is provided (the reportB64 itself stays
72
+ * OUTSIDE the signed body, exactly like the enclave output, so the
73
+ * report can be attached or swapped after signing without changing the
74
+ * canonical hash).
75
+ */
76
+ export async function signBody(
77
+ key: ManualKey,
78
+ artifact: BitGraphProof["artifact"],
79
+ commit: BitGraphProof["commit"],
80
+ measurement: string,
81
+ extras?: {
82
+ attribution?: { name?: string; title?: string; message?: string };
83
+ attestation?: { format: string; reportB64: string };
84
+ }
85
+ ): Promise<BitGraphProof> {
86
+ const signedBody: Record<string, unknown> = {
87
+ version: "bitgraph/1" as const,
88
+ artifact,
89
+ commit,
90
+ publicKeyB64: key.publicKeyB64,
91
+ enforcement: "stub" as const,
92
+ measurement,
93
+ };
94
+ if (extras?.attribution !== undefined) signedBody["attribution"] = extras.attribution;
95
+ if (extras?.attestation !== undefined) signedBody["attestationFormat"] = extras.attestation.format;
96
+ const signatureB64 = b64(await signAsync(canonicalize(signedBody), key.privateKey));
97
+ const proof: BitGraphProof = {
98
+ version: "bitgraph/1",
99
+ artifact,
100
+ commit,
101
+ signer: { publicKeyB64: key.publicKeyB64, signatureB64 },
102
+ environment: {
103
+ enforcement: "stub",
104
+ measurement,
105
+ ...(extras?.attestation !== undefined
106
+ ? { attestation: { format: extras.attestation.format, reportB64: extras.attestation.reportB64 } }
107
+ : {}),
108
+ },
109
+ };
110
+ if (extras?.attribution !== undefined) {
111
+ (proof as unknown as Record<string, unknown>)["attribution"] = extras.attribution;
112
+ }
113
+ return proof;
114
+ }
115
+
116
+ /**
117
+ * Build a proof through the root Constructor (the real commit path).
118
+ * Default fixtures are chainless: no counter, no epochId.
119
+ */
120
+ export async function makeConstructorProof(opts?: {
121
+ withCounter?: boolean;
122
+ epochId?: string;
123
+ payload?: string;
124
+ }): Promise<{ proof: BitGraphProof; bytes: Uint8Array }> {
125
+ const privateKey = crypto.getRandomValues(new Uint8Array(32));
126
+ const publicKeyBytes = await getPublicKeyAsync(privateKey);
127
+ const measurement = "test-measurement-audit";
128
+
129
+ let counter = 0;
130
+ const base = {
131
+ enforcementTier: "stub" as const,
132
+ getMeasurement: async () => measurement,
133
+ getFreshNonce: async () => crypto.getRandomValues(new Uint8Array(16)),
134
+ sign: async (data: Uint8Array) => signAsync(data, privateKey),
135
+ getPublicKey: async () => publicKeyBytes,
136
+ };
137
+ const host: HostCapabilities = opts?.withCounter
138
+ ? { ...base, nextCounter: async () => String(++counter) }
139
+ : base;
140
+
141
+ const ctor = await Constructor.initialize(
142
+ opts?.epochId !== undefined ? { host, epochId: opts.epochId } : { host }
143
+ );
144
+ const bytes = utf8(opts?.payload ?? "bitgraph-audit-test-payload");
145
+ const proof = await ctor.commit({ bytes });
146
+ return { proof, bytes };
147
+ }
148
+
149
+ /**
150
+ * Build a verifier-valid proof carrying a chainId inside the signed
151
+ * commit body, matching what the live enclave injects for non-default
152
+ * chains.
153
+ */
154
+ export async function makeChainIdProof(opts?: {
155
+ chainId?: string;
156
+ counter?: string;
157
+ epochId?: string;
158
+ payload?: string;
159
+ }): Promise<{ proof: BitGraphProof; bytes: Uint8Array }> {
160
+ const key = await makeKey();
161
+ const bytes = utf8(opts?.payload ?? "bitgraph-audit-chainid-payload");
162
+ const commit: BitGraphProof["commit"] = {
163
+ nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
164
+ ...(opts?.counter !== undefined ? { counter: opts.counter } : {}),
165
+ ...(opts?.epochId !== undefined ? { epochId: opts.epochId } : {}),
166
+ };
167
+ (commit as unknown as Record<string, unknown>)["chainId"] = opts?.chainId ?? "bitgraph:main";
168
+ const proof = await signBody(
169
+ key,
170
+ { hashAlg: "sha256", digestB64: b64(sha256(bytes)) },
171
+ commit,
172
+ "test-measurement-chainid"
173
+ );
174
+ return { proof, bytes };
175
+ }
176
+
177
+ /**
178
+ * Build a verifier-valid epoch-genesis proof carrying an epochLink that
179
+ * consumes the given predecessor into the given successor epoch.
180
+ *
181
+ * By default the predecessor key is a fresh random key; pass
182
+ * prevPublicKeyB64 to reference the actual signer of an observed
183
+ * predecessor. Optional counter, slotCounter, prevB64, and chainId land
184
+ * in the signed commit body, matching real enclave output shapes.
185
+ */
186
+ export async function makeEpochLinkProof(opts: {
187
+ prevEpochId: string;
188
+ prevCounter: string;
189
+ prevProofHashB64: string;
190
+ toEpochId: string;
191
+ prevPublicKeyB64?: string;
192
+ key?: ManualKey;
193
+ counter?: string;
194
+ slotCounter?: string;
195
+ prevB64?: string;
196
+ chainId?: string;
197
+ measurement?: string;
198
+ payload?: string;
199
+ random?: RandomSource;
200
+ }): Promise<{ proof: BitGraphProof; bytes: Uint8Array; proofHash: string; chainHash: string; key: ManualKey }> {
201
+ const random = opts.random ?? defaultRandom;
202
+ const key = opts.key ?? (await makeKey(random));
203
+ const prevPublicKeyB64 = opts.prevPublicKeyB64 ?? (await makeKey(random)).publicKeyB64;
204
+ const bytes = utf8(opts.payload ?? `bitgraph-audit-epochlink-${opts.toEpochId}`);
205
+ const commit: BitGraphProof["commit"] = {
206
+ nonceB64: b64(random(16)),
207
+ ...(opts.counter !== undefined ? { counter: opts.counter } : {}),
208
+ ...(opts.slotCounter !== undefined ? { slotCounter: opts.slotCounter } : {}),
209
+ ...(opts.prevB64 !== undefined ? { prevB64: opts.prevB64 } : {}),
210
+ epochId: opts.toEpochId,
211
+ epochLink: {
212
+ prevEpochId: opts.prevEpochId,
213
+ prevPublicKeyB64,
214
+ prevCounter: opts.prevCounter,
215
+ prevProofHashB64: opts.prevProofHashB64,
216
+ toEpochId: opts.toEpochId,
217
+ toPublicKeyB64: key.publicKeyB64,
218
+ },
219
+ };
220
+ if (opts.chainId !== undefined) {
221
+ (commit as unknown as Record<string, unknown>)["chainId"] = opts.chainId;
222
+ }
223
+ const proof = await signBody(
224
+ key,
225
+ { hashAlg: "sha256", digestB64: b64(sha256(bytes)) },
226
+ commit,
227
+ opts.measurement ?? "test-measurement-epochlink"
228
+ );
229
+ return { proof, bytes, proofHash: computeProofHash(proof), chainHash: computeChainHash(proof), key };
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Linked slot/commit counter chains (real enclave shape)
234
+ // ---------------------------------------------------------------------------
235
+
236
+ export interface CounterChainLink {
237
+ proof: BitGraphProof;
238
+ bytes: Uint8Array;
239
+ /** Signed-body identity hash (computeProofHash). */
240
+ proofHash: string;
241
+ /** Whole-proof chain hash (computeChainHash): the value a successor's prevB64 references. */
242
+ chainHash: string;
243
+ }
244
+
245
+ /**
246
+ * Build a linked chain of verifier-valid proofs under one signer key,
247
+ * epoch, and chain, with explicit counter positions per proof. Each
248
+ * non-first proof carries prevB64 equal to the canonical proof hash of
249
+ * its predecessor; the first proof omits prevB64 entirely (genesis per
250
+ * G1) unless prevB64OfFirst continues the chain from an existing proof.
251
+ * Pass pairs like real enclave output (slot 1/commit 2, slot 3/
252
+ * commit 4, ...) or leave positions out for counterless link chains.
253
+ */
254
+ export async function makeCounterChain(opts: {
255
+ epochId: string;
256
+ pairs: Array<{ slot?: string; commit?: string }>;
257
+ key?: ManualKey;
258
+ chainId?: string;
259
+ measurement?: string;
260
+ payloadPrefix?: string;
261
+ /** Continue an existing chain: the first proof carries this prevB64 instead of being a genesis. */
262
+ prevB64OfFirst?: string;
263
+ random?: RandomSource;
264
+ }): Promise<{ key: ManualKey; proofs: CounterChainLink[] }> {
265
+ const random = opts.random ?? defaultRandom;
266
+ const key = opts.key ?? (await makeKey(random));
267
+ const measurement = opts.measurement ?? "test-measurement-chain";
268
+ const proofs: CounterChainLink[] = [];
269
+
270
+ for (let i = 0; i < opts.pairs.length; i++) {
271
+ const pair = opts.pairs[i]!;
272
+ const bytes = utf8(`${opts.payloadPrefix ?? opts.epochId}-payload-${i}`);
273
+ const prevB64 = i > 0 ? proofs[i - 1]!.chainHash : opts.prevB64OfFirst;
274
+ const commit: BitGraphProof["commit"] = {
275
+ nonceB64: b64(random(16)),
276
+ ...(pair.commit !== undefined ? { counter: pair.commit } : {}),
277
+ ...(pair.slot !== undefined ? { slotCounter: pair.slot } : {}),
278
+ ...(prevB64 !== undefined ? { prevB64 } : {}),
279
+ epochId: opts.epochId,
280
+ };
281
+ if (opts.chainId !== undefined) {
282
+ (commit as unknown as Record<string, unknown>)["chainId"] = opts.chainId;
283
+ }
284
+ const proof = await signBody(
285
+ key,
286
+ { hashAlg: "sha256", digestB64: b64(sha256(bytes)) },
287
+ commit,
288
+ measurement
289
+ );
290
+ proofs.push({ proof, bytes, proofHash: computeProofHash(proof), chainHash: computeChainHash(proof) });
291
+ }
292
+
293
+ return { key, proofs };
294
+ }
295
+
296
+ /**
297
+ * The standard healthy enclave counter sequence: slot 1/commit 2,
298
+ * slot 3/commit 4, ... for `length` proofs.
299
+ */
300
+ export function healthyPairs(length: number): Array<{ slot: string; commit: string }> {
301
+ const pairs: Array<{ slot: string; commit: string }> = [];
302
+ for (let i = 0; i < length; i++) {
303
+ pairs.push({ slot: String(2 * i + 1), commit: String(2 * i + 2) });
304
+ }
305
+ return pairs;
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Ethereum anchor fixtures (matching packages/hosted/src/bitcoin-anchor.ts)
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /**
313
+ * Build a verifier-valid Ethereum anchor proof: signed attribution
314
+ * name "Ethereum Anchor", message = the block hash string, title = the
315
+ * Etherscan block URL, artifact digest = SHA-256 over the block-hash
316
+ * STRING (exactly like bitcoin-anchor.ts lines 188-189). Unsigned
317
+ * metadata is attached after signing when provided.
318
+ */
319
+ export async function makeAnchorProof(opts: {
320
+ blockHash: string;
321
+ blockNumber?: string | number;
322
+ /** Overrides the Etherscan URL entirely (for unparseable-title cases). */
323
+ title?: string;
324
+ /** Omit the title field entirely. */
325
+ noTitle?: boolean;
326
+ /** Overrides the artifact digest (for digest-binding failure cases; the proof stays validly signed). */
327
+ digestB64?: string;
328
+ key?: ManualKey;
329
+ epochId?: string;
330
+ counter?: string;
331
+ slotCounter?: string;
332
+ prevB64?: string;
333
+ chainId?: string;
334
+ measurement?: string;
335
+ /** Unsigned metadata object attached verbatim after signing. */
336
+ metadata?: unknown;
337
+ /** Overrides attribution.name (for non-anchor attribution cases). */
338
+ attributionName?: string;
339
+ random?: RandomSource;
340
+ }): Promise<{ proof: BitGraphProof; proofHash: string; chainHash: string; key: ManualKey }> {
341
+ const random = opts.random ?? defaultRandom;
342
+ const key = opts.key ?? (await makeKey(random));
343
+ const commit: BitGraphProof["commit"] = {
344
+ nonceB64: b64(random(16)),
345
+ ...(opts.counter !== undefined ? { counter: opts.counter } : {}),
346
+ ...(opts.slotCounter !== undefined ? { slotCounter: opts.slotCounter } : {}),
347
+ ...(opts.prevB64 !== undefined ? { prevB64: opts.prevB64 } : {}),
348
+ ...(opts.epochId !== undefined ? { epochId: opts.epochId } : {}),
349
+ };
350
+ if (opts.chainId !== undefined) {
351
+ (commit as unknown as Record<string, unknown>)["chainId"] = opts.chainId;
352
+ }
353
+ const attribution: { name: string; title?: string; message: string } = {
354
+ name: opts.attributionName ?? "Ethereum Anchor",
355
+ message: opts.blockHash,
356
+ };
357
+ if (opts.noTitle !== true) {
358
+ attribution.title = opts.title ?? `https://etherscan.io/block/${opts.blockNumber ?? 0}`;
359
+ }
360
+ const proof = await signBody(
361
+ key,
362
+ {
363
+ hashAlg: "sha256",
364
+ digestB64: opts.digestB64 ?? b64(sha256(utf8(opts.blockHash))),
365
+ },
366
+ commit,
367
+ opts.measurement ?? "test-measurement-anchor",
368
+ { attribution }
369
+ );
370
+ if (opts.metadata !== undefined) {
371
+ (proof as unknown as Record<string, unknown>)["metadata"] = opts.metadata;
372
+ }
373
+ return { proof, proofHash: computeProofHash(proof), chainHash: computeChainHash(proof), key };
374
+ }
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Minimal RLP encoder + synthetic Ethereum block headers (test side only;
378
+ // the audit package carries its own independent decoder)
379
+ // ---------------------------------------------------------------------------
380
+
381
+ export type RlpInput = Uint8Array | RlpInput[];
382
+
383
+ export function encodeRlp(item: RlpInput): Uint8Array {
384
+ if (item instanceof Uint8Array) {
385
+ if (item.length === 1 && (item[0] as number) < 0x80) return item;
386
+ return concatBytes([encodeRlpLength(item.length, 0x80), item]);
387
+ }
388
+ const payload = concatBytes(item.map(encodeRlp));
389
+ return concatBytes([encodeRlpLength(payload.length, 0xc0), payload]);
390
+ }
391
+
392
+ function encodeRlpLength(length: number, offset: number): Uint8Array {
393
+ if (length <= 55) return new Uint8Array([offset + length]);
394
+ const bytes: number[] = [];
395
+ let v = length;
396
+ while (v > 0) {
397
+ bytes.unshift(v & 0xff);
398
+ v = Math.floor(v / 256);
399
+ }
400
+ return new Uint8Array([offset + 55 + bytes.length, ...bytes]);
401
+ }
402
+
403
+ /** Minimal big-endian bytes of a non-negative integer; empty for zero (Ethereum header integer encoding). */
404
+ export function beBytes(value: number | bigint): Uint8Array {
405
+ let v = BigInt(value);
406
+ const bytes: number[] = [];
407
+ while (v > 0n) {
408
+ bytes.unshift(Number(v & 0xffn));
409
+ v >>= 8n;
410
+ }
411
+ return new Uint8Array(bytes);
412
+ }
413
+
414
+ /**
415
+ * Build a synthetic 20-item RLP block header with the number at index 8
416
+ * and the timestamp at index 11 (the stable Ethereum header positions).
417
+ * Other items are deterministic filler byte strings.
418
+ */
419
+ export function makeEthereumHeader(opts: {
420
+ blockNumber: number | bigint;
421
+ timestamp: number | bigint;
422
+ itemCount?: number;
423
+ }): { headerBytes: Uint8Array; headerRlpHex: string } {
424
+ const count = opts.itemCount ?? 20;
425
+ const items: RlpInput[] = [];
426
+ for (let i = 0; i < count; i++) {
427
+ if (i === 8) items.push(beBytes(opts.blockNumber));
428
+ else if (i === 11) items.push(beBytes(opts.timestamp));
429
+ else items.push(new Uint8Array(32).fill(i + 1));
430
+ }
431
+ const headerBytes = encodeRlp(items);
432
+ return {
433
+ headerBytes,
434
+ headerRlpHex: `0x${Buffer.from(headerBytes).toString("hex")}`,
435
+ };
436
+ }
437
+
438
+ /** JSON string of a bitgraph-anchor-witness/1 file. */
439
+ export function witnessJson(opts: {
440
+ headerRlpHex?: unknown;
441
+ blockNumber?: unknown;
442
+ blockHash?: unknown;
443
+ network?: string;
444
+ omit?: Array<"headerRlpHex" | "blockNumber" | "blockHash">;
445
+ }): string {
446
+ const witness: Record<string, unknown> = { version: "bitgraph-anchor-witness/1" };
447
+ if (!(opts.omit ?? []).includes("headerRlpHex")) witness["headerRlpHex"] = opts.headerRlpHex;
448
+ if (!(opts.omit ?? []).includes("blockNumber")) witness["blockNumber"] = opts.blockNumber;
449
+ if (!(opts.omit ?? []).includes("blockHash")) witness["blockHash"] = opts.blockHash;
450
+ if (opts.network !== undefined) witness["network"] = opts.network;
451
+ return JSON.stringify(witness);
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Bundle-on-disk helpers
456
+ // ---------------------------------------------------------------------------
457
+
458
+ export async function makeTempDir(prefix: string): Promise<string> {
459
+ return mkdtemp(join(tmpdir(), prefix));
460
+ }
461
+
462
+ /**
463
+ * Write a bundle directory from a path -> content map. Paths use "/"
464
+ * separators; parent directories are created as needed.
465
+ */
466
+ export async function writeBundleDir(
467
+ root: string,
468
+ files: Record<string, Uint8Array | string>
469
+ ): Promise<void> {
470
+ for (const [relPath, content] of Object.entries(files)) {
471
+ const abs = join(root, ...relPath.split("/"));
472
+ await mkdir(dirname(abs), { recursive: true });
473
+ await writeFile(abs, typeof content === "string" ? utf8(content) : content);
474
+ }
475
+ }
476
+
477
+ // ---------------------------------------------------------------------------
478
+ // Minimal in-memory tar writer
479
+ // ---------------------------------------------------------------------------
480
+
481
+ export interface TarWriteEntry {
482
+ /** Header name field (up to 100 bytes). */
483
+ name: string;
484
+ content: Uint8Array | string;
485
+ /** Defaults to "0" (regular file). */
486
+ typeflag?: string;
487
+ /** Emit a PAX extended header carrying path=<value> before the entry. */
488
+ paxPath?: string;
489
+ /**
490
+ * Override the size field written in the PAX 'x' header (defaults to the
491
+ * real record length). Used by ingest cap tests to declare an absurd
492
+ * metadata-entry size without writing that many bytes.
493
+ */
494
+ paxHeaderDeclaredSize?: number;
495
+ /** Emit a GNU long-name ('L') entry carrying this name before the entry. */
496
+ gnuLongName?: string;
497
+ }
498
+
499
+ /** Build a complete tar archive in memory (ustar, PAX, GNU long-name). */
500
+ export function makeTar(entries: TarWriteEntry[]): Uint8Array {
501
+ const parts: Uint8Array[] = [];
502
+ for (const entry of entries) {
503
+ const content = typeof entry.content === "string" ? utf8(entry.content) : entry.content;
504
+ if (entry.paxPath !== undefined) {
505
+ const record = paxRecord("path", entry.paxPath);
506
+ const declaredSize = entry.paxHeaderDeclaredSize ?? record.length;
507
+ parts.push(tarHeaderBlock("PaxHeaders.0/entry", declaredSize, "x"), padTo512(record));
508
+ }
509
+ if (entry.gnuLongName !== undefined) {
510
+ const nameBytes = utf8(`${entry.gnuLongName}\0`);
511
+ parts.push(tarHeaderBlock("././@LongLink", nameBytes.length, "L"), padTo512(nameBytes));
512
+ }
513
+ parts.push(tarHeaderBlock(entry.name, content.length, entry.typeflag ?? "0"), padTo512(content));
514
+ }
515
+ parts.push(new Uint8Array(512), new Uint8Array(512));
516
+ return concatBytes(parts);
517
+ }
518
+
519
+ function tarHeaderBlock(name: string, size: number, typeflag: string): Uint8Array {
520
+ const block = new Uint8Array(512);
521
+ const ascii = (text: string, offset: number, length: number): void => {
522
+ const bytes = utf8(text);
523
+ block.set(bytes.subarray(0, Math.min(bytes.length, length)), offset);
524
+ };
525
+ ascii(name, 0, 100);
526
+ block.set(octalField(0o644, 8), 100); // mode
527
+ block.set(octalField(0, 8), 108); // uid
528
+ block.set(octalField(0, 8), 116); // gid
529
+ block.set(octalField(size, 12), 124); // size
530
+ block.set(octalField(0, 12), 136); // mtime
531
+ for (let i = 148; i < 156; i++) block[i] = 0x20; // checksum spaces
532
+ block[156] = typeflag.charCodeAt(0);
533
+ ascii("ustar", 257, 6); // magic, NUL-terminated by the zero block
534
+ ascii("00", 263, 2); // version
535
+ let sum = 0;
536
+ for (let i = 0; i < 512; i++) sum += block[i] as number;
537
+ ascii(sum.toString(8).padStart(6, "0"), 148, 6);
538
+ block[154] = 0;
539
+ block[155] = 0x20;
540
+ return block;
541
+ }
542
+
543
+ function octalField(value: number, length: number): Uint8Array {
544
+ const out = new Uint8Array(length);
545
+ const text = value.toString(8).padStart(length - 1, "0");
546
+ for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i);
547
+ out[length - 1] = 0;
548
+ return out;
549
+ }
550
+
551
+ /** PAX record: "<len> <key>=<value>\n" where len counts the whole record. */
552
+ function paxRecord(key: string, value: string): Uint8Array {
553
+ const baseBytes = utf8(` ${key}=${value}\n`);
554
+ let length = baseBytes.length + 1;
555
+ while (String(length).length + baseBytes.length !== length) {
556
+ length = String(length).length + baseBytes.length;
557
+ }
558
+ return concatBytes([utf8(String(length)), baseBytes]);
559
+ }
560
+
561
+ function padTo512(content: Uint8Array): Uint8Array {
562
+ const padding = (512 - (content.length % 512)) % 512;
563
+ if (padding === 0) return content;
564
+ const out = new Uint8Array(content.length + padding);
565
+ out.set(content, 0);
566
+ return out;
567
+ }
568
+
569
+ function concatBytes(parts: Uint8Array[]): Uint8Array {
570
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
571
+ const out = new Uint8Array(total);
572
+ let offset = 0;
573
+ for (const part of parts) {
574
+ out.set(part, offset);
575
+ offset += part.length;
576
+ }
577
+ return out;
578
+ }
579
+
580
+ // ---------------------------------------------------------------------------
581
+ // Serialization helpers
582
+ // ---------------------------------------------------------------------------
583
+
584
+ /** Compact JSON serialization of a proof, the usual on-disk form. */
585
+ export function proofJson(proof: BitGraphProof): string {
586
+ return JSON.stringify(proof);
587
+ }
588
+
589
+ /** Stored form: the proof with a trailing proofHash field, as the ledger serves it. */
590
+ export function storedProofJson(proof: BitGraphProof, proofHash: string): string {
591
+ return JSON.stringify({ ...proof, proofHash });
592
+ }
593
+
594
+ // ---------------------------------------------------------------------------
595
+ // Standard mixed audit bundle (shared by the report and CLI suites)
596
+ // ---------------------------------------------------------------------------
597
+
598
+ export interface StandardAuditBundle {
599
+ dir: string;
600
+ epochId: string;
601
+ anchorEpochId: string;
602
+ chainId: string;
603
+ /** Canonical hashes of the four chain proofs, in chain order, INCLUDING the dropped one. */
604
+ chainProofHashes: string[];
605
+ /** Canonical hash of the chain proof deliberately absent from the bundle (index 1, slot 3/commit 4). */
606
+ droppedProofHash: string;
607
+ forkAProofHash: string;
608
+ forkBProofHash: string;
609
+ anchorProofHash: string;
610
+ blockNumber: number;
611
+ blockTimestamp: number;
612
+ /** Bundle path of the occ/1 reject. */
613
+ unsupportedPath: string;
614
+ /** Expected counts for assertions. */
615
+ expected: {
616
+ observed: number;
617
+ proofFiles: number;
618
+ exactDuplicates: number;
619
+ semanticDuplicates: number;
620
+ unsupportedVersion: number;
621
+ verified: number;
622
+ failed: number;
623
+ artifactUnavailable: number;
624
+ chainless: number;
625
+ };
626
+ }
627
+
628
+ /**
629
+ * Build the standard mixed bundle in a temp directory, per the Phase 4d
630
+ * brief: a healthy signed chain (4 proofs, slot/commit pairs 1..8) with
631
+ * one proof missing (index 1, producing an unexplained-position gap and
632
+ * a chain break), a predecessor-reuse fork off the chain tail, an occ/1
633
+ * unsupported-version reject, one artifact-present proof (chain genesis)
634
+ * and artifact-absent proofs (everything else), an Ethereum anchor in
635
+ * its own epoch with a valid offline witness, plus one exact-duplicate
636
+ * copy and one semantic-duplicate (stored-form) copy of the genesis.
637
+ */
638
+ export async function makeStandardAuditBundle(): Promise<StandardAuditBundle> {
639
+ const dir = await makeTempDir("bitgraph-audit-standard-");
640
+ const epochId = "epoch-standard-main";
641
+ const anchorEpochId = "epoch-standard-anchor";
642
+ const chainId = "bitgraph:main";
643
+ const measurement = "test-measurement-chain";
644
+
645
+ const chain = await makeCounterChain({
646
+ epochId,
647
+ pairs: healthyPairs(4),
648
+ chainId,
649
+ measurement,
650
+ payloadPrefix: "standard-bundle",
651
+ });
652
+
653
+ const tail = chain.proofs[3]!;
654
+ const forkChild = async (slot: string, commit: string, payload: string) => {
655
+ const bytes = utf8(payload);
656
+ const commitBody: BitGraphProof["commit"] = {
657
+ nonceB64: b64(crypto.getRandomValues(new Uint8Array(16))),
658
+ counter: commit,
659
+ slotCounter: slot,
660
+ prevB64: tail.chainHash,
661
+ epochId,
662
+ };
663
+ (commitBody as unknown as Record<string, unknown>)["chainId"] = chainId;
664
+ const proof = await signBody(
665
+ chain.key,
666
+ { hashAlg: "sha256", digestB64: b64(sha256(bytes)) },
667
+ commitBody,
668
+ measurement
669
+ );
670
+ return { proof, proofHash: computeProofHash(proof), chainHash: computeChainHash(proof) };
671
+ };
672
+ const forkA = await forkChild("9", "10", "standard-bundle-fork-a");
673
+ const forkB = await forkChild("11", "12", "standard-bundle-fork-b");
674
+
675
+ const blockNumber = 123456;
676
+ const blockTimestamp = 1_700_000_000;
677
+ const { headerBytes, headerRlpHex } = makeEthereumHeader({ blockNumber, timestamp: blockTimestamp });
678
+ const blockHash = `0x${Buffer.from(keccak_256(headerBytes)).toString("hex")}`;
679
+ const anchor = await makeAnchorProof({
680
+ blockHash,
681
+ blockNumber,
682
+ epochId: anchorEpochId,
683
+ counter: "2",
684
+ slotCounter: "1",
685
+ });
686
+
687
+ const unsupportedPath = "legacy/old.json";
688
+ const occProof = JSON.stringify({
689
+ version: "occ/1",
690
+ artifact: { hashAlg: "sha256", digestB64: "b2NjLWxlZ2FjeQ==" },
691
+ commit: { nonceB64: "b2NjLW5vbmNl" },
692
+ signer: { publicKeyB64: "b2NjLWtleQ==", signatureB64: "b2NjLXNpZw==" },
693
+ });
694
+
695
+ const genesis = chain.proofs[0]!;
696
+ await writeBundleDir(dir, {
697
+ "proofs/chain-0.json": proofJson(genesis.proof),
698
+ "proofs/chain-0-copy.json": proofJson(genesis.proof),
699
+ "proofs/chain-0-stored.json": storedProofJson(genesis.proof, genesis.proofHash),
700
+ // chain-1 (slot 3 / commit 4) is deliberately absent: the gap.
701
+ "proofs/chain-2.json": proofJson(chain.proofs[2]!.proof),
702
+ "proofs/chain-3.json": proofJson(tail.proof),
703
+ "proofs/fork-a.json": proofJson(forkA.proof),
704
+ "proofs/fork-b.json": proofJson(forkB.proof),
705
+ "proofs/anchor.json": proofJson(anchor.proof),
706
+ "witnesses/block.json": witnessJson({ headerRlpHex, blockNumber, blockHash }),
707
+ "artifacts/payload-0.bin": genesis.bytes,
708
+ [unsupportedPath]: occProof,
709
+ });
710
+
711
+ return {
712
+ dir,
713
+ epochId,
714
+ anchorEpochId,
715
+ chainId,
716
+ chainProofHashes: chain.proofs.map((p) => p.proofHash),
717
+ droppedProofHash: chain.proofs[1]!.proofHash,
718
+ forkAProofHash: forkA.proofHash,
719
+ forkBProofHash: forkB.proofHash,
720
+ anchorProofHash: anchor.proofHash,
721
+ blockNumber,
722
+ blockTimestamp,
723
+ unsupportedPath,
724
+ expected: {
725
+ observed: 6,
726
+ proofFiles: 8,
727
+ exactDuplicates: 1,
728
+ semanticDuplicates: 1,
729
+ unsupportedVersion: 1,
730
+ verified: 1,
731
+ failed: 0,
732
+ artifactUnavailable: 5,
733
+ chainless: 0,
734
+ },
735
+ };
736
+ }