@mikeargento/bitgraph 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +21 -21
  2. package/dist/fuse-cli.js +2 -2
  3. package/dist/fuse-cli.js.map +1 -1
  4. package/dist/fuse.d.ts +15 -1
  5. package/dist/fuse.d.ts.map +1 -1
  6. package/dist/fuse.js +29 -12
  7. package/dist/fuse.js.map +1 -1
  8. package/package.json +12 -8
  9. package/src/fuse-cli.ts +2 -2
  10. package/src/fuse.ts +46 -13
  11. package/dist/__tests__/audit-anchors.test.d.ts +0 -2
  12. package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
  13. package/dist/__tests__/audit-anchors.test.js +0 -152
  14. package/dist/__tests__/audit-anchors.test.js.map +0 -1
  15. package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
  16. package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
  17. package/dist/__tests__/audit-anomalies.test.js +0 -663
  18. package/dist/__tests__/audit-anomalies.test.js.map +0 -1
  19. package/dist/__tests__/audit-attestation.test.d.ts +0 -2
  20. package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
  21. package/dist/__tests__/audit-attestation.test.js +0 -403
  22. package/dist/__tests__/audit-attestation.test.js.map +0 -1
  23. package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
  24. package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
  25. package/dist/__tests__/audit-bench-smoke.test.js +0 -87
  26. package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
  27. package/dist/__tests__/audit-boundary.test.d.ts +0 -20
  28. package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
  29. package/dist/__tests__/audit-boundary.test.js +0 -84
  30. package/dist/__tests__/audit-boundary.test.js.map +0 -1
  31. package/dist/__tests__/audit-cli.test.d.ts +0 -2
  32. package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
  33. package/dist/__tests__/audit-cli.test.js +0 -239
  34. package/dist/__tests__/audit-cli.test.js.map +0 -1
  35. package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
  36. package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
  37. package/dist/__tests__/audit-dependencies.test.js +0 -116
  38. package/dist/__tests__/audit-dependencies.test.js.map +0 -1
  39. package/dist/__tests__/audit-fixtures.d.ts +0 -265
  40. package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
  41. package/dist/__tests__/audit-fixtures.js +0 -511
  42. package/dist/__tests__/audit-fixtures.js.map +0 -1
  43. package/dist/__tests__/audit-ingest.test.d.ts +0 -2
  44. package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
  45. package/dist/__tests__/audit-ingest.test.js +0 -473
  46. package/dist/__tests__/audit-ingest.test.js.map +0 -1
  47. package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
  48. package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
  49. package/dist/__tests__/audit-memory-ingest.test.js +0 -135
  50. package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
  51. package/dist/__tests__/audit-property.test.d.ts +0 -2
  52. package/dist/__tests__/audit-property.test.d.ts.map +0 -1
  53. package/dist/__tests__/audit-property.test.js +0 -771
  54. package/dist/__tests__/audit-property.test.js.map +0 -1
  55. package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
  56. package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
  57. package/dist/__tests__/audit-real-fixtures.test.js +0 -132
  58. package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
  59. package/dist/__tests__/audit-real-live.test.d.ts +0 -2
  60. package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
  61. package/dist/__tests__/audit-real-live.test.js +0 -107
  62. package/dist/__tests__/audit-real-live.test.js.map +0 -1
  63. package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
  64. package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
  65. package/dist/__tests__/audit-reconstruct.test.js +0 -287
  66. package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
  67. package/dist/__tests__/audit-report.test.d.ts +0 -2
  68. package/dist/__tests__/audit-report.test.d.ts.map +0 -1
  69. package/dist/__tests__/audit-report.test.js +0 -186
  70. package/dist/__tests__/audit-report.test.js.map +0 -1
  71. package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
  72. package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
  73. package/dist/__tests__/audit-roundtrip.test.js +0 -368
  74. package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
  75. package/dist/__tests__/audit-temporal.test.d.ts +0 -2
  76. package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
  77. package/dist/__tests__/audit-temporal.test.js +0 -364
  78. package/dist/__tests__/audit-temporal.test.js.map +0 -1
  79. package/dist/__tests__/audit-tiers.test.d.ts +0 -2
  80. package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
  81. package/dist/__tests__/audit-tiers.test.js +0 -282
  82. package/dist/__tests__/audit-tiers.test.js.map +0 -1
  83. package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
  84. package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
  85. package/dist/__tests__/audit-witness-real.test.js +0 -82
  86. package/dist/__tests__/audit-witness-real.test.js.map +0 -1
  87. package/dist/__tests__/audit-witness.test.d.ts +0 -2
  88. package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
  89. package/dist/__tests__/audit-witness.test.js +0 -308
  90. package/dist/__tests__/audit-witness.test.js.map +0 -1
  91. package/dist/__tests__/canonical.test.d.ts +0 -2
  92. package/dist/__tests__/canonical.test.d.ts.map +0 -1
  93. package/dist/__tests__/canonical.test.js +0 -162
  94. package/dist/__tests__/canonical.test.js.map +0 -1
  95. package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
  96. package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
  97. package/dist/__tests__/chain-hash-anchor.test.js +0 -55
  98. package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
  99. package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
  100. package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
  101. package/dist/__tests__/commit-service.integration.test.js +0 -95
  102. package/dist/__tests__/commit-service.integration.test.js.map +0 -1
  103. package/dist/__tests__/constructor.test.d.ts +0 -2
  104. package/dist/__tests__/constructor.test.d.ts.map +0 -1
  105. package/dist/__tests__/constructor.test.js +0 -267
  106. package/dist/__tests__/constructor.test.js.map +0 -1
  107. package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
  108. package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
  109. package/dist/__tests__/declared-proof-fixture.js +0 -66
  110. package/dist/__tests__/declared-proof-fixture.js.map +0 -1
  111. package/dist/__tests__/fuse-inline.test.d.ts +0 -2
  112. package/dist/__tests__/fuse-inline.test.d.ts.map +0 -1
  113. package/dist/__tests__/fuse-inline.test.js +0 -181
  114. package/dist/__tests__/fuse-inline.test.js.map +0 -1
  115. package/dist/__tests__/fuse-placement.test.d.ts +0 -2
  116. package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
  117. package/dist/__tests__/fuse-placement.test.js +0 -31
  118. package/dist/__tests__/fuse-placement.test.js.map +0 -1
  119. package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
  120. package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
  121. package/dist/__tests__/fuse-roundtrip.test.js +0 -241
  122. package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
  123. package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
  124. package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
  125. package/dist/__tests__/fuse-sdk.test.js +0 -117
  126. package/dist/__tests__/fuse-sdk.test.js.map +0 -1
  127. package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
  128. package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
  129. package/dist/__tests__/fuse-set-sdk.test.js +0 -721
  130. package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
  131. package/dist/__tests__/fuse-set.test.d.ts +0 -2
  132. package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
  133. package/dist/__tests__/fuse-set.test.js +0 -889
  134. package/dist/__tests__/fuse-set.test.js.map +0 -1
  135. package/dist/__tests__/fuse-set2.test.d.ts +0 -2
  136. package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
  137. package/dist/__tests__/fuse-set2.test.js +0 -294
  138. package/dist/__tests__/fuse-set2.test.js.map +0 -1
  139. package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
  140. package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
  141. package/dist/__tests__/fuse-vectors.test.js +0 -175
  142. package/dist/__tests__/fuse-vectors.test.js.map +0 -1
  143. package/dist/__tests__/fuse-verify.test.d.ts +0 -2
  144. package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
  145. package/dist/__tests__/fuse-verify.test.js +0 -250
  146. package/dist/__tests__/fuse-verify.test.js.map +0 -1
  147. package/dist/__tests__/hello-world.test.d.ts +0 -2
  148. package/dist/__tests__/hello-world.test.d.ts.map +0 -1
  149. package/dist/__tests__/hello-world.test.js +0 -9
  150. package/dist/__tests__/hello-world.test.js.map +0 -1
  151. package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
  152. package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
  153. package/dist/__tests__/mock-proof-fixture.js +0 -34
  154. package/dist/__tests__/mock-proof-fixture.js.map +0 -1
  155. package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
  156. package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
  157. package/dist/__tests__/policy-enforcement.test.js +0 -188
  158. package/dist/__tests__/policy-enforcement.test.js.map +0 -1
  159. package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
  160. package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
  161. package/dist/__tests__/proof-hash-regression.test.js +0 -95
  162. package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
  163. package/dist/__tests__/proof-hash.test.d.ts +0 -2
  164. package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
  165. package/dist/__tests__/proof-hash.test.js +0 -116
  166. package/dist/__tests__/proof-hash.test.js.map +0 -1
  167. package/dist/__tests__/proof-integrity.test.d.ts +0 -2
  168. package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
  169. package/dist/__tests__/proof-integrity.test.js +0 -454
  170. package/dist/__tests__/proof-integrity.test.js.map +0 -1
  171. package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
  172. package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
  173. package/dist/__tests__/realistic-proof-fixture.js +0 -57
  174. package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
  175. package/dist/__tests__/run.test.d.ts +0 -2
  176. package/dist/__tests__/run.test.d.ts.map +0 -1
  177. package/dist/__tests__/run.test.js +0 -178
  178. package/dist/__tests__/run.test.js.map +0 -1
  179. package/dist/__tests__/verifier.test.d.ts +0 -2
  180. package/dist/__tests__/verifier.test.d.ts.map +0 -1
  181. package/dist/__tests__/verifier.test.js +0 -531
  182. package/dist/__tests__/verifier.test.js.map +0 -1
  183. package/dist/canonical.d.ts +0 -54
  184. package/dist/canonical.d.ts.map +0 -1
  185. package/dist/canonical.js +0 -119
  186. package/dist/canonical.js.map +0 -1
  187. package/dist/proof-hash.d.ts +0 -9
  188. package/dist/proof-hash.d.ts.map +0 -1
  189. package/dist/proof-hash.js +0 -75
  190. package/dist/proof-hash.js.map +0 -1
  191. package/dist/types.d.ts +0 -705
  192. package/dist/types.d.ts.map +0 -1
  193. package/dist/types.js +0 -3
  194. package/dist/types.js.map +0 -1
  195. package/dist/verifier.d.ts +0 -27
  196. package/dist/verifier.d.ts.map +0 -1
  197. package/dist/verifier.js +0 -844
  198. package/dist/verifier.js.map +0 -1
  199. package/src/__tests__/audit-anchors.test.ts +0 -188
  200. package/src/__tests__/audit-anomalies.test.ts +0 -815
  201. package/src/__tests__/audit-attestation.test.ts +0 -529
  202. package/src/__tests__/audit-bench-smoke.test.ts +0 -108
  203. package/src/__tests__/audit-boundary.test.ts +0 -96
  204. package/src/__tests__/audit-cli.test.ts +0 -288
  205. package/src/__tests__/audit-dependencies.test.ts +0 -141
  206. package/src/__tests__/audit-fixtures.ts +0 -736
  207. package/src/__tests__/audit-ingest.test.ts +0 -575
  208. package/src/__tests__/audit-memory-ingest.test.ts +0 -156
  209. package/src/__tests__/audit-property.test.ts +0 -892
  210. package/src/__tests__/audit-real-fixtures.test.ts +0 -153
  211. package/src/__tests__/audit-real-live.test.ts +0 -128
  212. package/src/__tests__/audit-reconstruct.test.ts +0 -353
  213. package/src/__tests__/audit-report.test.ts +0 -222
  214. package/src/__tests__/audit-roundtrip.test.ts +0 -464
  215. package/src/__tests__/audit-temporal.test.ts +0 -446
  216. package/src/__tests__/audit-tiers.test.ts +0 -343
  217. package/src/__tests__/audit-witness-real.test.ts +0 -92
  218. package/src/__tests__/audit-witness.test.ts +0 -345
  219. package/src/__tests__/canonical.test.ts +0 -253
  220. package/src/__tests__/chain-hash-anchor.test.ts +0 -67
  221. package/src/__tests__/commit-service.integration.test.ts +0 -117
  222. package/src/__tests__/constructor.test.ts +0 -340
  223. package/src/__tests__/declared-proof-fixture.ts +0 -65
  224. package/src/__tests__/fuse-inline.test.ts +0 -211
  225. package/src/__tests__/fuse-placement.test.ts +0 -32
  226. package/src/__tests__/fuse-roundtrip.test.ts +0 -243
  227. package/src/__tests__/fuse-sdk.test.ts +0 -142
  228. package/src/__tests__/fuse-set-sdk.test.ts +0 -852
  229. package/src/__tests__/fuse-set.test.ts +0 -1027
  230. package/src/__tests__/fuse-set2.test.ts +0 -343
  231. package/src/__tests__/fuse-vectors.test.ts +0 -218
  232. package/src/__tests__/fuse-verify.test.ts +0 -278
  233. package/src/__tests__/hello-world.test.ts +0 -10
  234. package/src/__tests__/mock-proof-fixture.ts +0 -36
  235. package/src/__tests__/policy-enforcement.test.ts +0 -226
  236. package/src/__tests__/proof-hash-regression.test.ts +0 -107
  237. package/src/__tests__/proof-hash.test.ts +0 -129
  238. package/src/__tests__/proof-integrity.test.ts +0 -576
  239. package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
  240. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
  241. package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
  242. package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
  243. package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
  244. package/src/__tests__/real-fixtures/successor-112760.json +0 -38
  245. package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
  246. package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
  247. package/src/__tests__/realistic-proof-fixture.ts +0 -57
  248. package/src/__tests__/verifier.test.ts +0 -624
@@ -1,343 +0,0 @@
1
- // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
-
3
- /**
4
- * BitGraph Sets, set/2: a Merkle root over the rows instead of the list.
5
- * The tree, the root document, a member's evidence, and the verifier's
6
- * verdicts for a member with its path, without it, and with a stolen one.
7
- */
8
-
9
- import { describe, test } from "node:test";
10
- import * as assert from "node:assert/strict";
11
- import { readFileSync } from "node:fs";
12
- import { fileURLToPath } from "node:url";
13
- import { signAsync } from "@noble/ed25519";
14
- import { sha256 } from "@noble/hashes/sha256";
15
- import {
16
- PLACEMENTS,
17
- SET_METADATA_KEY,
18
- SET_MEMBER_METADATA_KEY,
19
- SET2_PLACEMENT_ID,
20
- MAX_SET2_MEMBERS,
21
- MerkleTree,
22
- buildSetManifest,
23
- buildSetMemberProof,
24
- buildSetRoot,
25
- buildSetTree,
26
- bytesEqual,
27
- bytesToBase64,
28
- bytesToHex,
29
- canonicalSlotBody,
30
- canonicalize,
31
- computeSlotCommitment,
32
- fuseAttribution,
33
- getPlacement,
34
- hexToBytes,
35
- merkleLeafHash,
36
- merklePath,
37
- merkleRoot,
38
- merkleRootFromPath,
39
- parseSetManifest,
40
- parseSetMemberProof,
41
- parseSetRoot,
42
- setLeaf,
43
- setMemberPath,
44
- setRootFromMember,
45
- verifyFuse,
46
- verifyFuseMember,
47
- } from "@mikeargento/bitgraph-verify";
48
- import type { BitGraphProof, SlotAllocation, Attribution, SetMember, SetMemberProof } from "@mikeargento/bitgraph-verify";
49
- import { makeKey, signBody, b64, utf8 } from "./audit-fixtures.js";
50
- import type { ManualKey } from "./audit-fixtures.js";
51
-
52
- const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
53
- const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
54
- const original = bytes("original.txt");
55
- const png = bytes("image.png");
56
- const dec = (b: Uint8Array) => new TextDecoder().decode(b);
57
- const EPOCH = bytesToBase64(new Uint8Array(32).fill(0x5e));
58
-
59
- async function allocateSlot(key: ManualKey, counter: string): Promise<SlotAllocation> {
60
- const body = { version: "bitgraph/slot/1" as const, nonceB64: b64(crypto.getRandomValues(new Uint8Array(32))), counter, epochId: EPOCH, publicKeyB64: key.publicKeyB64, chainId: "bitgraph:main" };
61
- return { ...body, signatureB64: b64(await signAsync(canonicalize(body), key.privateKey)) };
62
- }
63
-
64
- /** A set/2 proof: the root document is the artifact, set/2 the signed title, the slot consumed. */
65
- async function mintRootProof(o: { key: ManualKey; slot: SlotAllocation; commitCounter: string; rootDoc: Uint8Array; withMetadata?: boolean; attribution?: Attribution | null; member?: SetMemberProof }): Promise<BitGraphProof> {
66
- const commit: BitGraphProof["commit"] = { nonceB64: o.slot.nonceB64, counter: o.commitCounter, epochId: o.slot.epochId, slotCounter: o.slot.counter, slotHashB64: b64(sha256(canonicalize(canonicalSlotBody(o.slot)))) };
67
- (commit as unknown as Record<string, unknown>)["chainId"] = "bitgraph:main";
68
- const attribution = o.attribution === undefined ? fuseAttribution(SET2_PLACEMENT_ID) : o.attribution;
69
- const proof = await signBody(o.key, { hashAlg: "sha256", digestB64: b64(sha256(o.rootDoc)) }, commit, "test-measurement-set2", attribution === null ? undefined : { attribution });
70
- proof.slotAllocation = o.slot;
71
- if (o.withMetadata !== false) proof.metadata = { [SET_METADATA_KEY]: JSON.parse(dec(o.rootDoc)) as Record<string, unknown>, ...(o.member !== undefined ? { [SET_MEMBER_METADATA_KEY]: o.member } : {}) };
72
- return proof;
73
- }
74
-
75
- interface Fused { name: string; original: Uint8Array; placement: "trailer/1" | "container/1" | "container/2"; bytes: Uint8Array; row: SetMember }
76
- function fuseWith(commitment: Uint8Array, original: Uint8Array, placement: Fused["placement"], name: string): Fused {
77
- const fused = getPlacement(placement)!.build({ original, commitment });
78
- return { name, original, placement, bytes: fused, row: { artifact: sha256(fused), origin: sha256(original), placement } };
79
- }
80
-
81
- /** A set/2 of five members across the three member placements, minted under a fresh slot. */
82
- async function makeSet2() {
83
- const key = await makeKey();
84
- const slot = await allocateSlot(key, "40");
85
- const commitment = computeSlotCommitment(slot);
86
- const fused: Fused[] = [
87
- fuseWith(commitment, original, "trailer/1", "original.txt"),
88
- fuseWith(commitment, png, "container/1", "image.png"),
89
- fuseWith(commitment, utf8("a plain note for the tree\n"), "container/2", "note.txt"),
90
- fuseWith(commitment, utf8("a fourth member\n"), "trailer/1", "fourth.txt"),
91
- fuseWith(commitment, utf8("the fifth, in the new container\n"), "container/2", "fifth.txt"),
92
- ];
93
- const tree = buildSetTree(fused.map((f) => f.row));
94
- const rootDoc = buildSetRoot(commitment, tree.sorted.length, tree.root);
95
- const proof = await mintRootProof({ key, slot, commitCounter: "41", rootDoc });
96
- const evidenceOf = (f: Fused): SetMemberProof => {
97
- const k = tree.sorted.findIndex((r) => bytesEqual(r.artifact, f.row.artifact));
98
- return buildSetMemberProof(f.row, k, tree.sorted.length, tree.tree.path(k));
99
- };
100
- return { key, slot, commitment, fused, tree, rootDoc, proof, evidenceOf };
101
- }
102
-
103
- const leafInputs = ["", "00", "10", "2021", "3031", "40414243", "5051525354555657", "606162636465666768696a6b6c6d6e6f"].map((h) => hexToBytes(h) ?? new Uint8Array(0));
104
-
105
- describe("the tree (RFC 6962)", () => {
106
- test("1. the RFC 6962 vectors: one leaf, and the eight-leaf root", () => {
107
- const hashes = leafInputs.map(merkleLeafHash);
108
- assert.equal(bytesToHex(merkleRoot(hashes.slice(0, 1))), "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d");
109
- assert.equal(bytesToHex(merkleRoot(hashes)), "5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328");
110
- });
111
-
112
- test("2. every path of every tree up to 40 leaves recomputes the root, through both the recursive and the memoized tree", () => {
113
- for (let n = 1; n <= 40; n++) {
114
- const hashes = Array.from({ length: n }, (_, i) => merkleLeafHash(utf8(`leaf ${i} of ${n}`)));
115
- const root = merkleRoot(hashes);
116
- const tree = new MerkleTree(hashes);
117
- assert.deepEqual(tree.root, root, `tree root n=${n}`);
118
- for (let i = 0; i < n; i++) {
119
- const path = merklePath(hashes, i);
120
- assert.deepEqual(tree.path(i), path, `path n=${n} i=${i}`);
121
- assert.ok(path.length <= (n === 1 ? 0 : Math.ceil(Math.log2(n))), `path length n=${n} i=${i}`);
122
- assert.deepEqual(merkleRootFromPath(hashes[i]!, i, n, path), root, `recompute n=${n} i=${i}`);
123
- // A different index, a different size, a truncated or extended path, or a changed node never reaches the root.
124
- if (n > 1) {
125
- const other = (i + 1) % n;
126
- assert.notDeepEqual(merkleRootFromPath(hashes[i]!, other, n, path), root, `wrong index n=${n} i=${i}`);
127
- assert.notDeepEqual(merkleRootFromPath(hashes[i]!, i, n, path.slice(0, -1)) ?? new Uint8Array(0), root, `short path n=${n} i=${i}`);
128
- const tampered = path.map((p, j) => (j === 0 ? new Uint8Array(p.map((b, x) => (x === 0 ? b ^ 1 : b))) : p));
129
- assert.notDeepEqual(merkleRootFromPath(hashes[i]!, i, n, tampered), root, `tampered node n=${n} i=${i}`);
130
- }
131
- }
132
- // A path says nothing about the tree's size on its own: the same
133
- // siblings can close a tree of another size to the same root (three
134
- // leaves and a fourth whose subtree hash equals the third leaf). The
135
- // size is committed in the root document and the verifier binds the
136
- // evidence's count to it before the path is read.
137
- assert.equal(merkleRootFromPath(hashes[0]!, n, n, []), null, "index out of range");
138
- }
139
- });
140
-
141
- test("3. 20,000 leaves: the memoized tree builds and every path verifies in well under a few seconds", () => {
142
- const n = 20_000;
143
- const hashes = Array.from({ length: n }, (_, i) => merkleLeafHash(utf8(`leaf ${i}`)));
144
- const t0 = performance.now();
145
- const tree = new MerkleTree(hashes);
146
- let ok = 0;
147
- for (let i = 0; i < n; i++) if (bytesEqual(merkleRootFromPath(hashes[i]!, i, n, tree.path(i))!, tree.root)) ok++;
148
- const sec = (performance.now() - t0) / 1000;
149
- assert.equal(ok, n);
150
- assert.ok(sec < 10, `took ${sec.toFixed(1)} s`);
151
- });
152
- });
153
-
154
- describe("the root document and a member's evidence", () => {
155
- const commitment = new Uint8Array(32).fill(7);
156
- // Hex with letters in it, so an uppercase variant is a real change.
157
- const root = new Uint8Array(32).fill(0xab);
158
-
159
- test("4. buildSetRoot / parseSetRoot round trip; anything not byte-equal to its own rebuild is refused", () => {
160
- const doc = buildSetRoot(commitment, 44_000, root);
161
- assert.equal(dec(doc), `{"count":44000,"placement":"set/2","root":{"algorithm":"sha256","digest":"${bytesToHex(root)}"},"slotCommitment":{"algorithm":"sha256","digest":"${bytesToHex(commitment)}"},"type":"bitgraph-fuse/1"}`);
162
- const parsed = parseSetRoot(doc)!;
163
- assert.deepEqual(parsed.commitment, commitment);
164
- assert.deepEqual(parsed.root, root);
165
- assert.equal(parsed.count, 44_000);
166
- for (const bad of [
167
- dec(doc).replace("}", " }"),
168
- dec(doc).replace('"set/2"', '"set/1"'),
169
- dec(doc).replace('"count":44000', '"count":0'),
170
- dec(doc).replace('"count":44000', `"count":${MAX_SET2_MEMBERS + 1}`),
171
- dec(doc).replace('"count":44000', '"count":44000,"extra":1'),
172
- dec(doc).replace(bytesToHex(root), bytesToHex(root).toUpperCase()),
173
- ]) assert.equal(parseSetRoot(utf8(bad)), null, bad.slice(0, 60));
174
- assert.throws(() => buildSetRoot(commitment, 0, root));
175
- assert.throws(() => buildSetRoot(commitment, MAX_SET2_MEMBERS + 1, root));
176
- assert.throws(() => buildSetRoot(new Uint8Array(31), 1, root));
177
- });
178
-
179
- test("5. a member's evidence round-trips, and a malformed one reads as null", () => {
180
- const rows: SetMember[] = [0, 1, 2].map((i) => ({ artifact: new Uint8Array(32).fill(10 + i), origin: new Uint8Array(32).fill(20 + i), placement: "trailer/1" }));
181
- const tree = buildSetTree(rows);
182
- const ev = buildSetMemberProof(tree.sorted[1]!, 1, 3, tree.tree.path(1));
183
- assert.equal(ev.placement, "set/2");
184
- assert.equal(ev.count, 3);
185
- assert.equal(ev.index, 1);
186
- const back = parseSetMemberProof(JSON.parse(JSON.stringify(ev)))!;
187
- assert.deepEqual(back.member, tree.sorted[1]);
188
- assert.deepEqual(back.path, tree.tree.path(1));
189
- assert.deepEqual(setRootFromMember(back.member, back.index, back.count, back.path), tree.root);
190
- for (const mutate of [
191
- (o: Record<string, unknown>) => { o["index"] = 3; },
192
- (o: Record<string, unknown>) => { o["count"] = 0; },
193
- (o: Record<string, unknown>) => { (o["path"] as string[])[0] = "zz"; },
194
- (o: Record<string, unknown>) => { o["placement"] = "set/1"; },
195
- (o: Record<string, unknown>) => { (o["member"] as Record<string, unknown>)["placement"] = "set/2"; },
196
- (o: Record<string, unknown>) => { o["extra"] = true; },
197
- ]) {
198
- const o = JSON.parse(JSON.stringify(ev)) as Record<string, unknown>;
199
- mutate(o);
200
- assert.equal(parseSetMemberProof(o), null);
201
- }
202
- assert.throws(() => buildSetMemberProof(rows[0]!, 3, 3, []));
203
- });
204
-
205
- test("6. set/2 resolves through getPlacement as Form C and is not in PLACEMENTS; the attribution carries no origin; a manifest refuses a set/2 row", () => {
206
- assert.deepEqual(PLACEMENTS.map((p) => p.id), ["trailer/1", "container/1", "container/2", "produced/1"]);
207
- const p = getPlacement("set/2")!;
208
- assert.equal(p.form, "C");
209
- assert.equal(p.byteExact, false);
210
- const doc = buildSetRoot(commitment, 3, root);
211
- assert.deepEqual(p.locate(doc)!.commitment, commitment);
212
- assert.equal(p.locate(utf8("{}")), null);
213
- assert.throws(() => p.build({ commitment }));
214
- assert.deepEqual(fuseAttribution("set/2"), { name: "bitgraph-fuse/1", title: "set/2" });
215
- assert.throws(() => fuseAttribution("set/2", commitment), /no single origin/);
216
- const row: SetMember = { artifact: new Uint8Array(32).fill(1), origin: new Uint8Array(32).fill(2), placement: "set/2" };
217
- assert.throws(() => buildSetManifest(commitment, [row]), /cannot list a set/);
218
- assert.throws(() => setLeaf(row));
219
- const okRow: SetMember = { ...row, placement: "trailer/1" };
220
- const manifest = buildSetManifest(commitment, [okRow]);
221
- assert.ok(parseSetManifest(manifest));
222
- assert.equal(parseSetManifest(utf8(dec(manifest).replace('"placement":"trailer/1"', '"placement":"set/2"'))), null);
223
- });
224
- });
225
-
226
- describe("verifyFuseMember for a set/2 member", () => {
227
- test("7. every member: direct with its evidence, from its original with its evidence, and with the evidence riding under proof.metadata", async () => {
228
- const s = await makeSet2();
229
- for (const f of s.fused) {
230
- const ev = s.evidenceOf(f);
231
- const direct = await verifyFuseMember({ proof: s.proof, bytes: f.bytes, member: ev });
232
- assert.equal(direct.category, "SET_MEMBER_DIRECT", `${f.name}: ${direct.reason}`);
233
- assert.equal(direct.set!.kind, "set/2");
234
- assert.equal(direct.set!.memberCount, 5);
235
- assert.equal(direct.set!.memberSource, "argument");
236
- assert.equal(direct.set!.treeRootB64, bytesToBase64(s.tree.root));
237
- assert.equal(direct.set!.member!.index, ev.index);
238
- assert.equal(direct.placement, f.placement);
239
- assert.ok(direct.statements.some((t) => t.includes(`member ${ev.index + 1} of 5`)), direct.statements.join(" | "));
240
- assert.ok(direct.statements.some((t) => t.includes("recompute the committed root")));
241
- const fromOrigin = await verifyFuseMember({ proof: s.proof, bytes: f.original, member: ev });
242
- assert.equal(fromOrigin.category, "SET_MEMBER_FROM_ORIGIN", `${f.name}: ${fromOrigin.reason}`);
243
- assert.equal(fromOrigin.set!.member!.index, ev.index);
244
- const riding = await mintRootProof({ key: s.key, slot: s.slot, commitCounter: "41", rootDoc: s.rootDoc, member: ev });
245
- const viaMetadata = await verifyFuseMember({ proof: riding, bytes: f.bytes });
246
- assert.equal(viaMetadata.category, "SET_MEMBER_DIRECT");
247
- assert.equal(viaMetadata.set!.memberSource, "metadata");
248
- }
249
- // The root document itself is not a member; verifyFuse answers for it.
250
- const self = await verifyFuseMember({ proof: s.proof, bytes: s.rootDoc });
251
- assert.equal(self.category, "NO_MATCH");
252
- assert.match(self.reason ?? "", /root document itself/);
253
- const asFuse = await verifyFuse({ proof: s.proof, bytes: s.rootDoc });
254
- assert.equal(asFuse.category, "FUSED_DIRECT");
255
- assert.equal(asFuse.placement, "set/2");
256
- });
257
-
258
- test("8. without evidence the floor still shows but membership does not: SET_MEMBERSHIP_UNPROVEN for a member's own bytes and for the 51st file alike; an original without evidence is NO_MATCH", async () => {
259
- const s = await makeSet2();
260
- const own = await verifyFuseMember({ proof: s.proof, bytes: s.fused[0]!.bytes });
261
- assert.equal(own.category, "SET_MEMBERSHIP_UNPROVEN");
262
- assert.deepEqual(own.statements, []);
263
- assert.match(own.reason ?? "", /made after that slot existed/);
264
- assert.match(own.reason ?? "", /no member evidence/);
265
- assert.equal(own.set!.kind, "set/2");
266
- assert.equal(own.set!.member, null);
267
- const stray = getPlacement("trailer/1")!.build({ original: utf8("the 51st file\n"), commitment: s.commitment });
268
- const strayVerdict = await verifyFuseMember({ proof: s.proof, bytes: stray });
269
- assert.equal(strayVerdict.category, "SET_MEMBERSHIP_UNPROVEN");
270
- const plain = await verifyFuseMember({ proof: s.proof, bytes: s.fused[0]!.original });
271
- assert.equal(plain.category, "NO_MATCH");
272
- const foreign = getPlacement("trailer/1")!.build({ original: utf8("x\n"), commitment: new Uint8Array(32).fill(3) });
273
- assert.equal((await verifyFuseMember({ proof: s.proof, bytes: foreign })).category, "NO_MATCH");
274
- });
275
-
276
- test("9. a stolen path: the 51st file with a real member's evidence is INVALID_SET_PATH, as are a forged node, a wrong count, and evidence for another set", async () => {
277
- const s = await makeSet2();
278
- const ev = s.evidenceOf(s.fused[2]!);
279
- const stray = getPlacement("container/2")!.build({ original: utf8("the 51st file\n"), commitment: s.commitment });
280
- const stolen = await verifyFuseMember({ proof: s.proof, bytes: stray, member: ev });
281
- assert.equal(stolen.category, "INVALID_SET_PATH");
282
- assert.match(stolen.reason ?? "", /describes a different member/);
283
- assert.deepEqual(stolen.statements, []);
284
- const forged = JSON.parse(JSON.stringify(ev)) as SetMemberProof;
285
- if (forged.path.length > 0) forged.path[0] = forged.path[0]!.replace(/^[0-9a-f]/, (c) => (c === "a" ? "b" : "a"));
286
- else forged.index = 0;
287
- const forgedVerdict = await verifyFuseMember({ proof: s.proof, bytes: s.fused[2]!.bytes, member: forged });
288
- assert.equal(forgedVerdict.category, "INVALID_SET_PATH");
289
- const wrongCount = { ...JSON.parse(JSON.stringify(ev)), count: 6 } as SetMemberProof;
290
- assert.equal((await verifyFuseMember({ proof: s.proof, bytes: s.fused[2]!.bytes, member: wrongCount })).category, "INVALID_SET_PATH");
291
- const other = await makeSet2();
292
- const otherEv = other.evidenceOf(other.fused[2]!);
293
- assert.equal((await verifyFuseMember({ proof: s.proof, bytes: s.fused[2]!.bytes, member: otherEv })).category, "INVALID_SET_PATH");
294
- assert.equal((await verifyFuseMember({ proof: s.proof, bytes: s.fused[2]!.bytes, member: { nonsense: true } })).category, "INVALID_SET_PATH");
295
- });
296
-
297
- test("10. the root document is bound before any row: a wrong commitment, a stripped document, or a set/1 title over a root document are refused", async () => {
298
- const s = await makeSet2();
299
- const ev = s.evidenceOf(s.fused[0]!);
300
- const otherSlot = await allocateSlot(s.key, "50");
301
- const wrongDoc = buildSetRoot(computeSlotCommitment(otherSlot), 5, s.tree.root);
302
- const wrongCommitment = await mintRootProof({ key: s.key, slot: s.slot, commitCounter: "41", rootDoc: wrongDoc });
303
- assert.equal((await verifyFuseMember({ proof: wrongCommitment, bytes: s.fused[0]!.bytes, member: ev })).category, "INVALID_SLOT_COMMITMENT");
304
- const stripped = await mintRootProof({ key: s.key, slot: s.slot, commitCounter: "41", rootDoc: s.rootDoc, withMetadata: false });
305
- const noDoc = await verifyFuseMember({ proof: stripped, bytes: s.fused[0]!.bytes, member: ev });
306
- assert.equal(noDoc.category, "INVALID_SET_MANIFEST");
307
- assert.match(noDoc.reason ?? "", /root document/);
308
- const explicit = await verifyFuseMember({ proof: stripped, bytes: s.fused[0]!.bytes, member: ev, manifest: s.rootDoc });
309
- assert.equal(explicit.category, "SET_MEMBER_DIRECT", "explicit root document bytes bind a stripped proof");
310
- const asSet1 = await mintRootProof({ key: s.key, slot: s.slot, commitCounter: "41", rootDoc: s.rootDoc, attribution: fuseAttribution("set/1") });
311
- assert.equal((await verifyFuseMember({ proof: asSet1, bytes: s.fused[0]!.bytes, member: ev })).category, "INVALID_SET_MANIFEST");
312
- const tampered = JSON.parse(JSON.stringify(s.proof)) as BitGraphProof;
313
- (tampered.metadata![SET_METADATA_KEY] as Record<string, unknown>)["count"] = 6;
314
- assert.equal((await verifyFuseMember({ proof: tampered, bytes: s.fused[0]!.bytes, member: ev })).category, "INVALID_SET_MANIFEST");
315
- });
316
-
317
- test("11. an unregistered row placement in valid evidence is UNDETERMINED_PLACEMENT: the path proves the row, the bytes cannot be read", async () => {
318
- const key = await makeKey();
319
- const slot = await allocateSlot(key, "60");
320
- const commitment = computeSlotCommitment(slot);
321
- const real = fuseWith(commitment, original, "trailer/1", "a");
322
- const odd: SetMember = { artifact: new Uint8Array(32).fill(5), origin: sha256(utf8("odd")), placement: "xmp/9" };
323
- const tree = buildSetTree([real.row, odd]);
324
- const proof = await mintRootProof({ key, slot, commitCounter: "61", rootDoc: buildSetRoot(commitment, 2, tree.root) });
325
- const k = tree.sorted.findIndex((r) => r.placement === "xmp/9");
326
- const ev = buildSetMemberProof(odd, k, 2, tree.tree.path(k));
327
- // Bytes that hash to the odd row's artifact cannot exist here; a direct hit is not constructible, so the from-origin path reports the unregistered placement.
328
- const v = await verifyFuseMember({ proof, bytes: utf8("odd"), member: ev });
329
- assert.equal(v.category, "UNDETERMINED_PLACEMENT");
330
- const good = await verifyFuseMember({ proof, bytes: real.bytes, member: buildSetMemberProof(real.row, 1 - k, 2, tree.tree.path(1 - k)) });
331
- assert.equal(good.category, "SET_MEMBER_DIRECT");
332
- });
333
-
334
- test("12. paths from the tree helpers agree with the verifier: setMemberPath, setLeaf, setRootFromMember", async () => {
335
- const s = await makeSet2();
336
- for (let k = 0; k < s.tree.sorted.length; k++) {
337
- const row = s.tree.sorted[k]!;
338
- const path = setMemberPath(s.tree.tree, k);
339
- assert.deepEqual(merkleRootFromPath(setLeaf(row), k, 5, path), s.tree.root);
340
- assert.deepEqual(setRootFromMember(row, k, 5, path), s.tree.root);
341
- }
342
- });
343
- });
@@ -1,218 +0,0 @@
1
- // Copyright (c) Argento Computing Inc. All rights reserved. See LICENSE.
2
-
3
- /**
4
- * BitGraph Fuse: fixed vectors (spec 15.2).
5
- *
6
- * The slot record hash, the commitment preimage, the commitment, the Form C
7
- * payload bytes, the trailer bytes and the container bytes are pinned, and
8
- * the enclave's own commit.slotHashB64 on real fixture proofs must equal what
9
- * this package computes from the embedded slot record.
10
- */
11
-
12
- import { describe, test } from "node:test";
13
- import * as assert from "node:assert/strict";
14
- import { readFileSync } from "node:fs";
15
- import { fileURLToPath } from "node:url";
16
- import { sha256 } from "@noble/hashes/sha256";
17
- import {
18
- FUSE_DOMAIN,
19
- TRAILER_MAGIC,
20
- canonicalSlotBody,
21
- canonicalize,
22
- computeSlotRecordHash,
23
- slotCommitmentPreimage,
24
- computeSlotCommitment,
25
- buildFusePayload,
26
- parseFusePayload,
27
- getPlacement,
28
- bytesToHex,
29
- hexToBytes,
30
- bytesToBase64,
31
- base64ToBytes,
32
- } from "@mikeargento/bitgraph-verify";
33
- import type { BitGraphProof, SlotAllocation } from "@mikeargento/bitgraph-verify";
34
-
35
- const FIX = fileURLToPath(new URL("../../src/__tests__/fuse-fixtures/", import.meta.url));
36
- const bytes = (name: string) => new Uint8Array(readFileSync(FIX + name));
37
- const json = <T>(name: string): T => JSON.parse(readFileSync(FIX + name, "utf8")) as T;
38
-
39
- interface Vec { slot: SlotAllocation; canonicalSlotBody: string; slotRecordHashHex: string; preimageHex: string; commitmentHex: string }
40
- interface Vectors {
41
- domainHex: string;
42
- synthetic: Vec;
43
- trailerFixture: Vec & { fusedDigestB64: string; originDigestB64: string };
44
- containerFixture: Vec & { fusedDigestB64: string; originDigestB64: string };
45
- producedOriginFixture: Vec & { payload: string; fusedDigestB64: string };
46
- producedBareFixture: Vec & { payload: string; fusedDigestB64: string };
47
- }
48
- const V = json<Vectors>("vectors.json");
49
-
50
- describe("domain separation", () => {
51
- test("the domain is the 15 profile bytes followed by one zero byte, 16 bytes total", () => {
52
- assert.equal(FUSE_DOMAIN.length, 16);
53
- assert.equal(new TextDecoder().decode(FUSE_DOMAIN.subarray(0, 15)), "bitgraph-fuse/1");
54
- assert.equal(FUSE_DOMAIN[15], 0);
55
- assert.equal(bytesToHex(FUSE_DOMAIN), V.domainHex);
56
- });
57
-
58
- test("changing the domain label changes the commitment", () => {
59
- const slot = V.synthetic.slot;
60
- const nonce = base64ToBytes(slot.nonceB64)!;
61
- const other = new Uint8Array([...new TextEncoder().encode("bitgraph-fuse/2"), 0, ...computeSlotRecordHash(slot), ...nonce]);
62
- assert.notEqual(bytesToHex(sha256(other)), V.synthetic.commitmentHex);
63
- });
64
- });
65
-
66
- describe("slot record hash and commitment, synthetic vector", () => {
67
- const slot = V.synthetic.slot;
68
-
69
- test("canonical slot body is the enclave's field subset, sorted, compact", () => {
70
- const body = new TextDecoder().decode(canonicalize(canonicalSlotBody(slot)));
71
- assert.equal(body, V.synthetic.canonicalSlotBody);
72
- assert.equal(body, `{"chainId":"bitgraph:main","counter":"7","epochId":"${slot.epochId}","nonceB64":"${slot.nonceB64}","publicKeyB64":"${slot.publicKeyB64}","version":"bitgraph/slot/1"}`);
73
- assert.ok(!body.includes("signatureB64"), "the signature is outside the hashed body");
74
- });
75
-
76
- test("slotRecordHash, preimage and commitment are pinned and structurally related", () => {
77
- const hash = computeSlotRecordHash(slot);
78
- assert.equal(bytesToHex(hash), V.synthetic.slotRecordHashHex);
79
- const pre = slotCommitmentPreimage(slot);
80
- assert.equal(bytesToHex(pre), V.synthetic.preimageHex);
81
- assert.equal(pre.length, 16 + 32 + 32);
82
- assert.deepEqual(pre.subarray(0, 16), FUSE_DOMAIN);
83
- assert.deepEqual(pre.subarray(16, 48), hash);
84
- assert.deepEqual(pre.subarray(48, 80), base64ToBytes(slot.nonceB64));
85
- assert.equal(bytesToHex(computeSlotCommitment(slot)), V.synthetic.commitmentHex);
86
- assert.equal(bytesToHex(sha256(pre)), V.synthetic.commitmentHex);
87
- });
88
-
89
- test("one-bit mutations of the nonce or any signed slot field change the commitment", () => {
90
- const base = bytesToHex(computeSlotCommitment(slot));
91
- const nonce = base64ToBytes(slot.nonceB64)!;
92
- nonce[0] = nonce[0]! ^ 1;
93
- assert.notEqual(bytesToHex(computeSlotCommitment({ ...slot, nonceB64: bytesToBase64(nonce) })), base);
94
- assert.notEqual(bytesToHex(computeSlotCommitment({ ...slot, counter: "8" })), base);
95
- assert.notEqual(bytesToHex(computeSlotCommitment({ ...slot, epochId: bytesToBase64(new Uint8Array(32).fill(9)) })), base);
96
- assert.notEqual(bytesToHex(computeSlotCommitment({ ...slot, chainId: "global" })), base);
97
- const { chainId: _c, ...noChain } = slot;
98
- assert.notEqual(bytesToHex(computeSlotCommitment(noChain as SlotAllocation)), base, "a record without chainId is a different record");
99
- // The signature is not part of the hashed body, so changing it changes nothing here.
100
- assert.equal(bytesToHex(computeSlotCommitment({ ...slot, signatureB64: bytesToBase64(new Uint8Array(64).fill(7)) })), base);
101
- });
102
-
103
- test("a nonce that is not 32 bytes is refused", () => {
104
- assert.throws(() => computeSlotCommitment({ ...slot, nonceB64: bytesToBase64(new Uint8Array(16)) }), /32 bytes/);
105
- assert.throws(() => computeSlotCommitment({ ...slot, nonceB64: "not base64!" }), /32 bytes/);
106
- });
107
- });
108
-
109
- describe("real fixture proofs: the enclave agrees with this package", () => {
110
- for (const name of ["trailer", "container", "produced-origin", "produced-bare", "trailer-undeclared", "wrong-slot", "unregistered", "order-fused-during-hold"]) {
111
- test(`${name}: SHA-256 of the canonical slot body equals commit.slotHashB64`, () => {
112
- const proof = json<BitGraphProof>(`${name}.proof.json`);
113
- assert.ok(proof.slotAllocation, "fixture carries its slot record");
114
- assert.equal(bytesToBase64(computeSlotRecordHash(proof.slotAllocation!)), proof.commit.slotHashB64);
115
- assert.equal(proof.slotAllocation!.nonceB64, proof.commit.nonceB64);
116
- });
117
- }
118
-
119
- test("the fixture vectors match the embedded slot records", () => {
120
- for (const [name, v] of [["trailer", V.trailerFixture], ["container", V.containerFixture], ["produced-origin", V.producedOriginFixture], ["produced-bare", V.producedBareFixture]] as const) {
121
- const proof = json<BitGraphProof>(`${name}.proof.json`);
122
- assert.equal(bytesToHex(computeSlotCommitment(proof.slotAllocation!)), v.commitmentHex, name);
123
- assert.equal(proof.artifact.digestB64, v.fusedDigestB64, name);
124
- }
125
- });
126
- });
127
-
128
- describe("Form C payload bytes", () => {
129
- test("pinned bytes: sorted keys, compact, lowercase hex, type last", () => {
130
- const v = V.producedOriginFixture;
131
- const commitment = hexToBytes(v.commitmentHex)!;
132
- const origin = base64ToBytes(V.trailerFixture.originDigestB64)!;
133
- const built = buildFusePayload(commitment, origin);
134
- assert.equal(new TextDecoder().decode(built), v.payload);
135
- assert.equal(v.payload, `{"origin":{"algorithm":"sha256","digest":"${bytesToHex(origin)}"},"slotCommitment":{"algorithm":"sha256","digest":"${v.commitmentHex}"},"type":"bitgraph-fuse/1"}`);
136
- assert.equal(bytesToBase64(sha256(built)), v.fusedDigestB64);
137
- const bare = buildFusePayload(hexToBytes(V.producedBareFixture.commitmentHex)!);
138
- assert.equal(new TextDecoder().decode(bare), V.producedBareFixture.payload);
139
- assert.ok(!V.producedBareFixture.payload.includes("origin"));
140
- });
141
-
142
- test("parse round-trips and rejects every non-canonical or malformed form", () => {
143
- const good = bytes("produced-origin.json");
144
- const parsed = parseFusePayload(good)!;
145
- assert.equal(bytesToHex(parsed.commitment), V.producedOriginFixture.commitmentHex);
146
- assert.equal(bytesToBase64(parsed.originDigest!), V.trailerFixture.originDigestB64);
147
- const text = new TextDecoder().decode(good);
148
- const enc = (s: string) => new TextEncoder().encode(s);
149
- assert.equal(parseFusePayload(enc(JSON.stringify(JSON.parse(text), null, 2))), null, "whitespace");
150
- assert.equal(parseFusePayload(enc(text.replace("bitgraph-fuse/1", "bitgraph-fuse/2"))), null, "type");
151
- assert.equal(parseFusePayload(enc(text.replace(/"digest":"([0-9a-f]{8})/, (_m, h: string) => `"digest":"${h.toUpperCase()}`))), null, "uppercase hex");
152
- assert.equal(parseFusePayload(enc(text.replace("{\"origin\"", "{\"extra\":1,\"origin\""))), null, "extra key");
153
- assert.equal(parseFusePayload(enc(text.replace("\"type\":\"bitgraph-fuse/1\"", "\"type\":\"bitgraph-fuse/1\",\"type\":\"bitgraph-fuse/1\""))), null, "duplicate key");
154
- assert.equal(parseFusePayload(enc(text.replace("sha256", "sha512"))), null, "algorithm");
155
- assert.equal(parseFusePayload(enc("[]")), null);
156
- assert.equal(parseFusePayload(new Uint8Array([0xff, 0xfe])), null, "not UTF-8");
157
- });
158
- });
159
-
160
- describe("trailer/1 and container/1 bytes", () => {
161
- test("trailer is original || 'BGFUSE01' || 8 zero bytes || commitment", () => {
162
- const fused = bytes("fused-trailer.bin");
163
- const original = bytes("original.txt");
164
- assert.equal(fused.length, original.length + 48);
165
- assert.deepEqual(fused.subarray(0, original.length), original);
166
- assert.equal(new TextDecoder().decode(fused.subarray(original.length, original.length + 8)), TRAILER_MAGIC);
167
- assert.ok(fused.subarray(original.length + 8, original.length + 16).every((b) => b === 0));
168
- assert.equal(bytesToHex(fused.subarray(original.length + 16)), V.trailerFixture.commitmentHex);
169
- assert.equal(bytesToBase64(sha256(fused)), V.trailerFixture.fusedDigestB64);
170
- });
171
-
172
- test("container is a fixed-layout ustar: manifest, original, two zero blocks, all metadata zeroed", () => {
173
- const fused = bytes("fused-container.tar");
174
- const png = bytes("image.png");
175
- assert.equal(fused.length % 512, 0);
176
- const name = (off: number) => new TextDecoder().decode(fused.subarray(off, off + 100)).replace(/\0+$/, "");
177
- assert.equal(name(0), "bitgraph-fuse/manifest.json");
178
- const manifestLen = parseInt(new TextDecoder().decode(fused.subarray(124, 135)), 8);
179
- const manifest = fused.subarray(512, 512 + manifestLen);
180
- const parsed = parseFusePayload(manifest)!;
181
- assert.equal(bytesToHex(parsed.commitment), V.containerFixture.commitmentHex);
182
- assert.equal(bytesToBase64(parsed.originDigest!), V.containerFixture.originDigestB64);
183
- const second = 512 + manifestLen + ((512 - (manifestLen % 512)) % 512);
184
- assert.equal(name(second), "bitgraph-fuse/original");
185
- assert.equal(new TextDecoder().decode(fused.subarray(second + 136, second + 147)), "00000000000", "mtime is zero");
186
- assert.deepEqual(fused.subarray(second + 512, second + 512 + png.length), png);
187
- assert.ok(fused.subarray(fused.length - 1024).every((b) => b === 0), "two zero blocks end the archive");
188
- assert.equal(bytesToBase64(sha256(fused)), V.containerFixture.fusedDigestB64);
189
- const rebuilt = getPlacement("container/1")!.build({ original: png, commitment: parsed.commitment });
190
- assert.deepEqual(rebuilt, fused);
191
- });
192
-
193
- test("one-bit mutation of the fused bytes changes the artifact digest", () => {
194
- for (const name of ["fused-trailer.bin", "fused-container.tar", "produced-origin.json"]) {
195
- const f = bytes(name);
196
- const d0 = bytesToBase64(sha256(f));
197
- f[0] = f[0]! ^ 1;
198
- assert.notEqual(bytesToBase64(sha256(f)), d0, name);
199
- }
200
- });
201
- });
202
-
203
- describe("encoding helpers", () => {
204
- test("base64 is strict standard alphabet with canonical padding", () => {
205
- assert.deepEqual(base64ToBytes("AQID"), new Uint8Array([1, 2, 3]));
206
- assert.equal(base64ToBytes("AQI"), null, "length");
207
- assert.equal(base64ToBytes("AQI-"), null, "url-safe alphabet");
208
- assert.equal(base64ToBytes("AQ==\n"), null, "whitespace");
209
- assert.equal(base64ToBytes("AR=="), null, "non-canonical trailing bits");
210
- assert.equal(bytesToBase64(new Uint8Array(32).fill(255)), "//////////////////////////////////////////8=");
211
- assert.deepEqual(base64ToBytes("//////////////////////////////////////////8="), new Uint8Array(32).fill(255));
212
- });
213
- test("hex is lowercase only", () => {
214
- assert.deepEqual(hexToBytes("00ff"), new Uint8Array([0, 255]));
215
- assert.equal(hexToBytes("00FF"), null);
216
- assert.equal(hexToBytes("0"), null);
217
- });
218
- });