@mikeargento/bitgraph 1.9.0 → 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 (252) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +58 -35
  3. package/dist/constructor.js +1 -1
  4. package/dist/constructor.js.map +1 -1
  5. package/dist/fuse-cli.js +1 -1
  6. package/dist/fuse-cli.js.map +1 -1
  7. package/dist/fuse.js +1 -1
  8. package/dist/fuse.js.map +1 -1
  9. package/dist/host.js +1 -1
  10. package/dist/host.js.map +1 -1
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/policy.js +1 -1
  16. package/dist/policy.js.map +1 -1
  17. package/package.json +23 -8
  18. package/src/constructor.ts +1 -1
  19. package/src/fuse-cli.ts +1 -1
  20. package/src/fuse.ts +1 -1
  21. package/src/host.ts +1 -1
  22. package/src/index.ts +3 -2
  23. package/src/policy.ts +1 -1
  24. package/dist/__tests__/audit-anchors.test.d.ts +0 -2
  25. package/dist/__tests__/audit-anchors.test.d.ts.map +0 -1
  26. package/dist/__tests__/audit-anchors.test.js +0 -152
  27. package/dist/__tests__/audit-anchors.test.js.map +0 -1
  28. package/dist/__tests__/audit-anomalies.test.d.ts +0 -2
  29. package/dist/__tests__/audit-anomalies.test.d.ts.map +0 -1
  30. package/dist/__tests__/audit-anomalies.test.js +0 -663
  31. package/dist/__tests__/audit-anomalies.test.js.map +0 -1
  32. package/dist/__tests__/audit-attestation.test.d.ts +0 -2
  33. package/dist/__tests__/audit-attestation.test.d.ts.map +0 -1
  34. package/dist/__tests__/audit-attestation.test.js +0 -403
  35. package/dist/__tests__/audit-attestation.test.js.map +0 -1
  36. package/dist/__tests__/audit-bench-smoke.test.d.ts +0 -2
  37. package/dist/__tests__/audit-bench-smoke.test.d.ts.map +0 -1
  38. package/dist/__tests__/audit-bench-smoke.test.js +0 -87
  39. package/dist/__tests__/audit-bench-smoke.test.js.map +0 -1
  40. package/dist/__tests__/audit-boundary.test.d.ts +0 -20
  41. package/dist/__tests__/audit-boundary.test.d.ts.map +0 -1
  42. package/dist/__tests__/audit-boundary.test.js +0 -84
  43. package/dist/__tests__/audit-boundary.test.js.map +0 -1
  44. package/dist/__tests__/audit-cli.test.d.ts +0 -2
  45. package/dist/__tests__/audit-cli.test.d.ts.map +0 -1
  46. package/dist/__tests__/audit-cli.test.js +0 -239
  47. package/dist/__tests__/audit-cli.test.js.map +0 -1
  48. package/dist/__tests__/audit-dependencies.test.d.ts +0 -2
  49. package/dist/__tests__/audit-dependencies.test.d.ts.map +0 -1
  50. package/dist/__tests__/audit-dependencies.test.js +0 -116
  51. package/dist/__tests__/audit-dependencies.test.js.map +0 -1
  52. package/dist/__tests__/audit-fixtures.d.ts +0 -265
  53. package/dist/__tests__/audit-fixtures.d.ts.map +0 -1
  54. package/dist/__tests__/audit-fixtures.js +0 -511
  55. package/dist/__tests__/audit-fixtures.js.map +0 -1
  56. package/dist/__tests__/audit-ingest.test.d.ts +0 -2
  57. package/dist/__tests__/audit-ingest.test.d.ts.map +0 -1
  58. package/dist/__tests__/audit-ingest.test.js +0 -473
  59. package/dist/__tests__/audit-ingest.test.js.map +0 -1
  60. package/dist/__tests__/audit-memory-ingest.test.d.ts +0 -12
  61. package/dist/__tests__/audit-memory-ingest.test.d.ts.map +0 -1
  62. package/dist/__tests__/audit-memory-ingest.test.js +0 -135
  63. package/dist/__tests__/audit-memory-ingest.test.js.map +0 -1
  64. package/dist/__tests__/audit-property.test.d.ts +0 -2
  65. package/dist/__tests__/audit-property.test.d.ts.map +0 -1
  66. package/dist/__tests__/audit-property.test.js +0 -771
  67. package/dist/__tests__/audit-property.test.js.map +0 -1
  68. package/dist/__tests__/audit-real-fixtures.test.d.ts +0 -2
  69. package/dist/__tests__/audit-real-fixtures.test.d.ts.map +0 -1
  70. package/dist/__tests__/audit-real-fixtures.test.js +0 -132
  71. package/dist/__tests__/audit-real-fixtures.test.js.map +0 -1
  72. package/dist/__tests__/audit-real-live.test.d.ts +0 -2
  73. package/dist/__tests__/audit-real-live.test.d.ts.map +0 -1
  74. package/dist/__tests__/audit-real-live.test.js +0 -107
  75. package/dist/__tests__/audit-real-live.test.js.map +0 -1
  76. package/dist/__tests__/audit-reconstruct.test.d.ts +0 -2
  77. package/dist/__tests__/audit-reconstruct.test.d.ts.map +0 -1
  78. package/dist/__tests__/audit-reconstruct.test.js +0 -287
  79. package/dist/__tests__/audit-reconstruct.test.js.map +0 -1
  80. package/dist/__tests__/audit-report.test.d.ts +0 -2
  81. package/dist/__tests__/audit-report.test.d.ts.map +0 -1
  82. package/dist/__tests__/audit-report.test.js +0 -186
  83. package/dist/__tests__/audit-report.test.js.map +0 -1
  84. package/dist/__tests__/audit-roundtrip.test.d.ts +0 -2
  85. package/dist/__tests__/audit-roundtrip.test.d.ts.map +0 -1
  86. package/dist/__tests__/audit-roundtrip.test.js +0 -368
  87. package/dist/__tests__/audit-roundtrip.test.js.map +0 -1
  88. package/dist/__tests__/audit-temporal.test.d.ts +0 -2
  89. package/dist/__tests__/audit-temporal.test.d.ts.map +0 -1
  90. package/dist/__tests__/audit-temporal.test.js +0 -364
  91. package/dist/__tests__/audit-temporal.test.js.map +0 -1
  92. package/dist/__tests__/audit-tiers.test.d.ts +0 -2
  93. package/dist/__tests__/audit-tiers.test.d.ts.map +0 -1
  94. package/dist/__tests__/audit-tiers.test.js +0 -282
  95. package/dist/__tests__/audit-tiers.test.js.map +0 -1
  96. package/dist/__tests__/audit-witness-real.test.d.ts +0 -19
  97. package/dist/__tests__/audit-witness-real.test.d.ts.map +0 -1
  98. package/dist/__tests__/audit-witness-real.test.js +0 -82
  99. package/dist/__tests__/audit-witness-real.test.js.map +0 -1
  100. package/dist/__tests__/audit-witness.test.d.ts +0 -2
  101. package/dist/__tests__/audit-witness.test.d.ts.map +0 -1
  102. package/dist/__tests__/audit-witness.test.js +0 -308
  103. package/dist/__tests__/audit-witness.test.js.map +0 -1
  104. package/dist/__tests__/canonical.test.d.ts +0 -2
  105. package/dist/__tests__/canonical.test.d.ts.map +0 -1
  106. package/dist/__tests__/canonical.test.js +0 -162
  107. package/dist/__tests__/canonical.test.js.map +0 -1
  108. package/dist/__tests__/chain-hash-anchor.test.d.ts +0 -22
  109. package/dist/__tests__/chain-hash-anchor.test.d.ts.map +0 -1
  110. package/dist/__tests__/chain-hash-anchor.test.js +0 -55
  111. package/dist/__tests__/chain-hash-anchor.test.js.map +0 -1
  112. package/dist/__tests__/commit-service.integration.test.d.ts +0 -2
  113. package/dist/__tests__/commit-service.integration.test.d.ts.map +0 -1
  114. package/dist/__tests__/commit-service.integration.test.js +0 -95
  115. package/dist/__tests__/commit-service.integration.test.js.map +0 -1
  116. package/dist/__tests__/constructor.test.d.ts +0 -2
  117. package/dist/__tests__/constructor.test.d.ts.map +0 -1
  118. package/dist/__tests__/constructor.test.js +0 -267
  119. package/dist/__tests__/constructor.test.js.map +0 -1
  120. package/dist/__tests__/declared-proof-fixture.d.ts +0 -66
  121. package/dist/__tests__/declared-proof-fixture.d.ts.map +0 -1
  122. package/dist/__tests__/declared-proof-fixture.js +0 -66
  123. package/dist/__tests__/declared-proof-fixture.js.map +0 -1
  124. package/dist/__tests__/fuse-placement.test.d.ts +0 -2
  125. package/dist/__tests__/fuse-placement.test.d.ts.map +0 -1
  126. package/dist/__tests__/fuse-placement.test.js +0 -31
  127. package/dist/__tests__/fuse-placement.test.js.map +0 -1
  128. package/dist/__tests__/fuse-roundtrip.test.d.ts +0 -2
  129. package/dist/__tests__/fuse-roundtrip.test.d.ts.map +0 -1
  130. package/dist/__tests__/fuse-roundtrip.test.js +0 -241
  131. package/dist/__tests__/fuse-roundtrip.test.js.map +0 -1
  132. package/dist/__tests__/fuse-sdk.test.d.ts +0 -2
  133. package/dist/__tests__/fuse-sdk.test.d.ts.map +0 -1
  134. package/dist/__tests__/fuse-sdk.test.js +0 -117
  135. package/dist/__tests__/fuse-sdk.test.js.map +0 -1
  136. package/dist/__tests__/fuse-set-sdk.test.d.ts +0 -2
  137. package/dist/__tests__/fuse-set-sdk.test.d.ts.map +0 -1
  138. package/dist/__tests__/fuse-set-sdk.test.js +0 -721
  139. package/dist/__tests__/fuse-set-sdk.test.js.map +0 -1
  140. package/dist/__tests__/fuse-set.test.d.ts +0 -2
  141. package/dist/__tests__/fuse-set.test.d.ts.map +0 -1
  142. package/dist/__tests__/fuse-set.test.js +0 -868
  143. package/dist/__tests__/fuse-set.test.js.map +0 -1
  144. package/dist/__tests__/fuse-set2.test.d.ts +0 -2
  145. package/dist/__tests__/fuse-set2.test.d.ts.map +0 -1
  146. package/dist/__tests__/fuse-set2.test.js +0 -294
  147. package/dist/__tests__/fuse-set2.test.js.map +0 -1
  148. package/dist/__tests__/fuse-vectors.test.d.ts +0 -2
  149. package/dist/__tests__/fuse-vectors.test.d.ts.map +0 -1
  150. package/dist/__tests__/fuse-vectors.test.js +0 -175
  151. package/dist/__tests__/fuse-vectors.test.js.map +0 -1
  152. package/dist/__tests__/fuse-verify.test.d.ts +0 -2
  153. package/dist/__tests__/fuse-verify.test.d.ts.map +0 -1
  154. package/dist/__tests__/fuse-verify.test.js +0 -250
  155. package/dist/__tests__/fuse-verify.test.js.map +0 -1
  156. package/dist/__tests__/hello-world.test.d.ts +0 -2
  157. package/dist/__tests__/hello-world.test.d.ts.map +0 -1
  158. package/dist/__tests__/hello-world.test.js +0 -9
  159. package/dist/__tests__/hello-world.test.js.map +0 -1
  160. package/dist/__tests__/mock-proof-fixture.d.ts +0 -15
  161. package/dist/__tests__/mock-proof-fixture.d.ts.map +0 -1
  162. package/dist/__tests__/mock-proof-fixture.js +0 -34
  163. package/dist/__tests__/mock-proof-fixture.js.map +0 -1
  164. package/dist/__tests__/policy-enforcement.test.d.ts +0 -2
  165. package/dist/__tests__/policy-enforcement.test.d.ts.map +0 -1
  166. package/dist/__tests__/policy-enforcement.test.js +0 -188
  167. package/dist/__tests__/policy-enforcement.test.js.map +0 -1
  168. package/dist/__tests__/proof-hash-regression.test.d.ts +0 -2
  169. package/dist/__tests__/proof-hash-regression.test.d.ts.map +0 -1
  170. package/dist/__tests__/proof-hash-regression.test.js +0 -95
  171. package/dist/__tests__/proof-hash-regression.test.js.map +0 -1
  172. package/dist/__tests__/proof-hash.test.d.ts +0 -2
  173. package/dist/__tests__/proof-hash.test.d.ts.map +0 -1
  174. package/dist/__tests__/proof-hash.test.js +0 -116
  175. package/dist/__tests__/proof-hash.test.js.map +0 -1
  176. package/dist/__tests__/proof-integrity.test.d.ts +0 -2
  177. package/dist/__tests__/proof-integrity.test.d.ts.map +0 -1
  178. package/dist/__tests__/proof-integrity.test.js +0 -426
  179. package/dist/__tests__/proof-integrity.test.js.map +0 -1
  180. package/dist/__tests__/realistic-proof-fixture.d.ts +0 -56
  181. package/dist/__tests__/realistic-proof-fixture.d.ts.map +0 -1
  182. package/dist/__tests__/realistic-proof-fixture.js +0 -57
  183. package/dist/__tests__/realistic-proof-fixture.js.map +0 -1
  184. package/dist/__tests__/verifier.test.d.ts +0 -2
  185. package/dist/__tests__/verifier.test.d.ts.map +0 -1
  186. package/dist/__tests__/verifier.test.js +0 -531
  187. package/dist/__tests__/verifier.test.js.map +0 -1
  188. package/dist/canonical.d.ts +0 -54
  189. package/dist/canonical.d.ts.map +0 -1
  190. package/dist/canonical.js +0 -119
  191. package/dist/canonical.js.map +0 -1
  192. package/dist/proof-hash.d.ts +0 -9
  193. package/dist/proof-hash.d.ts.map +0 -1
  194. package/dist/proof-hash.js +0 -75
  195. package/dist/proof-hash.js.map +0 -1
  196. package/dist/types.d.ts +0 -705
  197. package/dist/types.d.ts.map +0 -1
  198. package/dist/types.js +0 -3
  199. package/dist/types.js.map +0 -1
  200. package/dist/verifier.d.ts +0 -27
  201. package/dist/verifier.d.ts.map +0 -1
  202. package/dist/verifier.js +0 -844
  203. package/dist/verifier.js.map +0 -1
  204. package/src/__tests__/audit-anchors.test.ts +0 -188
  205. package/src/__tests__/audit-anomalies.test.ts +0 -815
  206. package/src/__tests__/audit-attestation.test.ts +0 -529
  207. package/src/__tests__/audit-bench-smoke.test.ts +0 -108
  208. package/src/__tests__/audit-boundary.test.ts +0 -96
  209. package/src/__tests__/audit-cli.test.ts +0 -288
  210. package/src/__tests__/audit-dependencies.test.ts +0 -141
  211. package/src/__tests__/audit-fixtures.ts +0 -736
  212. package/src/__tests__/audit-ingest.test.ts +0 -575
  213. package/src/__tests__/audit-memory-ingest.test.ts +0 -156
  214. package/src/__tests__/audit-property.test.ts +0 -892
  215. package/src/__tests__/audit-real-fixtures.test.ts +0 -153
  216. package/src/__tests__/audit-real-live.test.ts +0 -128
  217. package/src/__tests__/audit-reconstruct.test.ts +0 -353
  218. package/src/__tests__/audit-report.test.ts +0 -222
  219. package/src/__tests__/audit-roundtrip.test.ts +0 -464
  220. package/src/__tests__/audit-temporal.test.ts +0 -446
  221. package/src/__tests__/audit-tiers.test.ts +0 -343
  222. package/src/__tests__/audit-witness-real.test.ts +0 -92
  223. package/src/__tests__/audit-witness.test.ts +0 -345
  224. package/src/__tests__/canonical.test.ts +0 -253
  225. package/src/__tests__/chain-hash-anchor.test.ts +0 -67
  226. package/src/__tests__/commit-service.integration.test.ts +0 -117
  227. package/src/__tests__/constructor.test.ts +0 -340
  228. package/src/__tests__/declared-proof-fixture.ts +0 -65
  229. package/src/__tests__/fuse-placement.test.ts +0 -32
  230. package/src/__tests__/fuse-roundtrip.test.ts +0 -243
  231. package/src/__tests__/fuse-sdk.test.ts +0 -142
  232. package/src/__tests__/fuse-set-sdk.test.ts +0 -852
  233. package/src/__tests__/fuse-set.test.ts +0 -1005
  234. package/src/__tests__/fuse-set2.test.ts +0 -343
  235. package/src/__tests__/fuse-vectors.test.ts +0 -218
  236. package/src/__tests__/fuse-verify.test.ts +0 -278
  237. package/src/__tests__/hello-world.test.ts +0 -10
  238. package/src/__tests__/mock-proof-fixture.ts +0 -36
  239. package/src/__tests__/policy-enforcement.test.ts +0 -226
  240. package/src/__tests__/proof-hash-regression.test.ts +0 -107
  241. package/src/__tests__/proof-hash.test.ts +0 -129
  242. package/src/__tests__/proof-integrity.test.ts +0 -541
  243. package/src/__tests__/real-fixtures/anchor-after-112762.json +0 -47
  244. package/src/__tests__/real-fixtures/anchor-with-ethereum-112758.json +0 -47
  245. package/src/__tests__/real-fixtures/live-proof-0.json +0 -42
  246. package/src/__tests__/real-fixtures/live-proof-1.json +0 -43
  247. package/src/__tests__/real-fixtures/live-proof-2.json +0 -43
  248. package/src/__tests__/real-fixtures/successor-112760.json +0 -38
  249. package/src/__tests__/real-fixtures/witness-25525146.json +0 -6
  250. package/src/__tests__/real-fixtures/witness-25525147.json +0 -6
  251. package/src/__tests__/realistic-proof-fixture.ts +0 -57
  252. package/src/__tests__/verifier.test.ts +0 -624
@@ -1,343 +0,0 @@
1
- // Copyright (c) 2024-2026 Mike Argento. 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) Mike Argento. 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
- });