enigma-memory 0.1.18 → 0.1.22

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 (276) hide show
  1. package/README.md +76 -24
  2. package/apps/cli/bin/enigma-desktop.mjs +140 -0
  3. package/apps/cli/bin/enigma-terminal.mjs +78 -0
  4. package/apps/cli/bin/enigma.mjs +1923 -285
  5. package/apps/desktop/electron-main.cjs +217 -0
  6. package/apps/desktop/package.json +12 -0
  7. package/apps/desktop/src/app.js +264 -7
  8. package/apps/desktop/src/index.html +3514 -1373
  9. package/apps/desktop/src/launch-electron.mjs +51 -0
  10. package/apps/desktop/src/server.mjs +2914 -0
  11. package/apps/desktop/src/styles.css +2972 -260
  12. package/apps/desktop/src/zk-browser-prove.mjs +53 -0
  13. package/apps/desktop/src/zk-state.mjs +1789 -0
  14. package/apps/gateway/bin/enigma-gateway.mjs +102 -5
  15. package/apps/gateway/src/server.mjs +271 -8
  16. package/apps/ios/EnigmaCore/Package.swift +12 -0
  17. package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
  18. package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
  19. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
  20. package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
  21. package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
  22. package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
  23. package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
  24. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
  25. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
  26. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
  27. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
  28. package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
  29. package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
  30. package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
  31. package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
  32. package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
  33. package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
  34. package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
  35. package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
  36. package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
  37. package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
  38. package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
  39. package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
  40. package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
  41. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
  42. package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
  43. package/apps/native-host/README.md +19 -8
  44. package/apps/native-host/bin/enigma-native-host.mjs +229 -13
  45. package/apps/relay/bin/enigma-relay.mjs +103 -5
  46. package/apps/relay/src/federation-runtime.mjs +618 -0
  47. package/apps/relay/src/server.mjs +310 -9
  48. package/apps/verifier/bin/enigma-verify.mjs +327 -11
  49. package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
  50. package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
  51. package/cortex-v3/circuits/build/vk_bytes.json +32 -0
  52. package/cortex-v3/proving-assets.json +64 -0
  53. package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
  54. package/cortex-v3/zk/action-transition-vk.json +119 -0
  55. package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
  56. package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
  57. package/cortex-v3/zk/intent-witness.mjs +365 -0
  58. package/cortex-v3/zk/intent-witness.test.mjs +485 -0
  59. package/cortex-v3/zk/proving-assets.mjs +203 -0
  60. package/cortex-v3/zk/sale-witness.mjs +783 -0
  61. package/cortex-v3/zk/sale-witness.test.mjs +784 -0
  62. package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
  63. package/cortex-v3/zk/settlement-evidence.mjs +722 -0
  64. package/cortex-v3/zk/setup-intent.mjs +688 -0
  65. package/cortex-v3/zk/setup-sale.mjs +666 -0
  66. package/cortex-v3/zk/setup.mjs +594 -0
  67. package/cortex-v3/zk/witness.mjs +184 -0
  68. package/cortex-v3/zk/zk-codec.mjs +232 -0
  69. package/cortex-v3/zk/zk-codec.test.mjs +293 -0
  70. package/cortex-v3/zk/zk-settle.mjs +370 -0
  71. package/cortex-v3/zk/zk-tree.mjs +256 -0
  72. package/cortex-v3/zk/zk-tree.test.mjs +419 -0
  73. package/deploy/docker-compose.local-production-simulation.yml +36 -0
  74. package/docs/browser-extension-install.md +8 -6
  75. package/docs/client-connectors.md +15 -11
  76. package/docs/developer-ecosystem.md +15 -13
  77. package/docs/enigma-memory-ready-conformance.md +11 -9
  78. package/docs/install-anywhere.md +61 -28
  79. package/docs/installers-and-desktop.md +8 -7
  80. package/docs/novelty-invention-candidates.md +161 -161
  81. package/docs/proof-network-claim-boundaries.md +320 -318
  82. package/examples/01-quickstart-agent/index.mjs +49 -0
  83. package/examples/01_agent_memory_quickstart.mjs +57 -0
  84. package/examples/02-multi-agent-swarm/index.mjs +57 -0
  85. package/examples/02_cross_model_passport.mjs +64 -0
  86. package/examples/03-langchain-memory/index.mjs +41 -0
  87. package/examples/03_poseidon_commitment_verification.mjs +71 -0
  88. package/examples/04-python-trading-agent/trader.py +49 -0
  89. package/examples/README.md +27 -0
  90. package/examples/ci/github-actions.yml +7 -2
  91. package/package.json +142 -11
  92. package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
  93. package/packages/connectors/src/index.js +196 -4
  94. package/packages/connectors/swarm-router.mjs +168 -0
  95. package/packages/core/src/index.js +248 -1
  96. package/packages/core/src/version.mjs +7 -0
  97. package/packages/dev-tools/package.json +19 -0
  98. package/packages/dev-tools/src/index.js +4 -0
  99. package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
  100. package/packages/dev-tools/src/swarm-simulator.js +101 -0
  101. package/packages/dev-tools/src/vault-inspector.js +114 -0
  102. package/packages/dev-tools/src/vector-benchmark.js +100 -0
  103. package/packages/developer-platform/src/access-credentials.js +341 -0
  104. package/packages/developer-platform/src/http.js +132 -0
  105. package/packages/developer-platform/src/index.js +4 -0
  106. package/packages/developer-platform/src/usage-http.js +60 -0
  107. package/packages/developer-platform/src/usage.js +295 -0
  108. package/packages/enclave-runtime/attestation.mjs +159 -0
  109. package/packages/enclave-runtime/index.mjs +47 -0
  110. package/packages/enclave-runtime/session-manager.mjs +253 -0
  111. package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
  112. package/packages/enigma-reflex/package.json +14 -0
  113. package/packages/enigma-reflex/src/index.js +204 -0
  114. package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
  115. package/packages/enigma-reflex/training/requirements.txt +8 -0
  116. package/packages/enigma-reflex/training/train.py +314 -0
  117. package/packages/enigma-weave/LICENSE +22 -0
  118. package/packages/enigma-weave/UPSTREAM.json +21 -0
  119. package/packages/enigma-weave/package.json +14 -0
  120. package/packages/enigma-weave/src/index.js +286 -0
  121. package/packages/hosted-cloud/src/index.js +80 -5
  122. package/packages/importers/src/index.js +432 -0
  123. package/packages/inference-runtime/src/browser.js +401 -0
  124. package/packages/inference-runtime/src/chat.js +265 -0
  125. package/packages/inference-runtime/src/code.js +407 -0
  126. package/packages/inference-runtime/src/contracts.js +162 -0
  127. package/packages/inference-runtime/src/http.js +232 -0
  128. package/packages/inference-runtime/src/image.js +186 -0
  129. package/packages/inference-runtime/src/index.js +10 -0
  130. package/packages/inference-runtime/src/model-router.js +320 -0
  131. package/packages/inference-runtime/src/platform.js +125 -0
  132. package/packages/inference-runtime/src/privacy.js +400 -0
  133. package/packages/inference-runtime/src/video.js +253 -0
  134. package/packages/mcp-server/README.md +22 -6
  135. package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
  136. package/packages/mcp-server/src/index.js +1418 -105
  137. package/packages/mcp-server/src/oauth.js +561 -0
  138. package/packages/mcp-server/src/private-handoff.js +84 -0
  139. package/packages/mcp-server/src/remote-http.js +273 -0
  140. package/packages/mcp-server/src/remote-policy.js +72 -0
  141. package/packages/mcp-server/swarm-bridge.mjs +361 -0
  142. package/packages/mesh/index.d.ts +283 -0
  143. package/packages/mesh/package.json +23 -0
  144. package/packages/mesh/src/crypto.js +189 -0
  145. package/packages/mesh/src/federation-packets.js +353 -0
  146. package/packages/mesh/src/gossip.js +311 -0
  147. package/packages/mesh/src/index.js +6 -0
  148. package/packages/mesh/src/protocol.js +255 -0
  149. package/packages/mesh/src/router.js +279 -0
  150. package/packages/mesh/src/transport.js +306 -0
  151. package/packages/passport/src/index.js +426 -1
  152. package/packages/private-economy/src/credits-http.js +100 -0
  153. package/packages/private-economy/src/credits.js +447 -0
  154. package/packages/private-economy/src/index.js +5 -0
  155. package/packages/private-economy/src/payments-http.js +120 -0
  156. package/packages/private-economy/src/payments.js +509 -0
  157. package/packages/private-economy/src/x402.js +346 -0
  158. package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
  159. package/packages/rag/index.d.ts +182 -0
  160. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
  161. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
  162. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
  163. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
  164. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
  165. package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
  166. package/packages/rag/package.json +27 -0
  167. package/packages/rag/src/blinded-search.js +109 -0
  168. package/packages/rag/src/bm25.js +169 -0
  169. package/packages/rag/src/embeddings.js +459 -0
  170. package/packages/rag/src/hybrid.js +76 -0
  171. package/packages/rag/src/index.js +38 -0
  172. package/packages/rag/src/reranker.js +61 -0
  173. package/packages/rag/src/research.js +107 -0
  174. package/packages/rag/src/vector-store.js +430 -0
  175. package/packages/rag/src/verify-model-artifacts.mjs +4 -0
  176. package/packages/sdk/index.d.ts +760 -0
  177. package/packages/sdk/package.json +33 -0
  178. package/packages/sdk/python/README.md +24 -0
  179. package/packages/sdk/python/enigma_sdk.py +250 -0
  180. package/packages/sdk/python/pyproject.toml +34 -0
  181. package/packages/sdk/python/requirements.txt +1 -0
  182. package/packages/sdk/python/setup.py +20 -0
  183. package/packages/sdk/src/federation/capability-grant.js +389 -0
  184. package/packages/sdk/src/federation/federation-router.js +360 -0
  185. package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
  186. package/packages/sdk/src/federation/index.js +3 -0
  187. package/packages/sdk/src/index.js +1796 -0
  188. package/packages/sdk/src/intelligence/contradiction.js +337 -0
  189. package/packages/sdk/src/intelligence/decision-engine.js +155 -0
  190. package/packages/sdk/src/intelligence/index.js +4 -0
  191. package/packages/sdk/src/intelligence/ontology.js +122 -0
  192. package/packages/sdk/src/intelligence/temporal.js +123 -0
  193. package/packages/sdk/src/market-client.js +142 -0
  194. package/packages/sdk/src/mesh-client.js +110 -0
  195. package/packages/sdk/src/middleware/index.js +3 -0
  196. package/packages/sdk/src/middleware/langchain.js +159 -0
  197. package/packages/sdk/src/middleware/llamaindex.js +101 -0
  198. package/packages/sdk/src/middleware/vercel-ai.js +112 -0
  199. package/packages/sdk/src/rag-client.js +85 -0
  200. package/packages/sdk/src/swarm-orchestrator.js +260 -0
  201. package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
  202. package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
  203. package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
  204. package/packages/snapcompact/index.d.ts +284 -0
  205. package/packages/snapcompact/package.json +25 -0
  206. package/packages/snapcompact/src/index.js +716 -0
  207. package/packages/storage/PACKAGE_CONTRACT.md +1 -1
  208. package/packages/terminal-console/animations.mjs +240 -0
  209. package/packages/terminal-console/auto-anchor.mjs +220 -0
  210. package/packages/terminal-console/banner.mjs +91 -0
  211. package/packages/terminal-console/commands.mjs +459 -0
  212. package/packages/terminal-console/delegation.mjs +152 -0
  213. package/packages/terminal-console/index.mjs +5 -0
  214. package/packages/terminal-console/outbox.mjs +143 -0
  215. package/packages/terminal-console/phantom-bridge.mjs +637 -0
  216. package/packages/terminal-console/repl.mjs +136 -0
  217. package/packages/terminal-console/signer-store.mjs +130 -0
  218. package/packages/terminal-console/solana-rpc.mjs +214 -0
  219. package/packages/terminal-console/solana-transport.mjs +189 -0
  220. package/packages/terminal-tui/dashboard.mjs +214 -0
  221. package/packages/terminal-tui/index.mjs +28 -0
  222. package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
  223. package/packages/terminal-tui/telemetry-hud.mjs +137 -0
  224. package/packages/vault/index.d.ts +449 -0
  225. package/packages/vault/package.json +27 -0
  226. package/packages/vault/src/e2ee.mjs +393 -0
  227. package/packages/vault/src/enclave.js +481 -0
  228. package/packages/vault/src/erasure.js +207 -0
  229. package/packages/vault/src/index.js +1018 -155
  230. package/packages/vault/src/persistence.js +307 -0
  231. package/packages/vault/src/poseidon.js +354 -0
  232. package/packages/vault/src/receipt.js +459 -0
  233. package/scripts/benchmark-optical-context.mjs +166 -0
  234. package/scripts/bootstrap-enigma.mjs +502 -0
  235. package/scripts/build-edge-backend-workers.mjs +20 -5
  236. package/scripts/build-goal-completion-audit.mjs +72 -25
  237. package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
  238. package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
  239. package/scripts/build-hosted-probe-worker.mjs +19 -4
  240. package/scripts/build-installer-assets.mjs +41 -21
  241. package/scripts/build-operator-evidence-starter.mjs +59 -1
  242. package/scripts/build-production-backend-env-kit.mjs +2 -0
  243. package/scripts/build-production-unblocker.mjs +3 -0
  244. package/scripts/check.mjs +17 -3
  245. package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
  246. package/scripts/install-enigma-local.mjs +18 -5
  247. package/scripts/release-audit.mjs +65 -109
  248. package/scripts/release-provenance.mjs +6 -0
  249. package/scripts/run-backend-readiness-smoke.mjs +112 -10
  250. package/scripts/scan-secrets.mjs +1 -0
  251. package/scripts/simulate-production-env.mjs +7 -2
  252. package/scripts/validate-hosted-backend-live.mjs +112 -1
  253. package/specs/antibody-pack-v1.schema.json +95 -0
  254. package/specs/antigen-envelope-v1.schema.json +81 -0
  255. package/specs/boundary-manifest-v1.schema.json +35 -35
  256. package/specs/capsule-v1.schema.json +55 -55
  257. package/specs/claim-boundary-manifest-v1.schema.json +22 -22
  258. package/specs/claim-ledger-v1.schema.json +291 -0
  259. package/specs/context-passport-v1.schema.json +59 -0
  260. package/specs/deletion-tombstone-v1.schema.json +26 -26
  261. package/specs/evidence-packet-v1.schema.json +177 -0
  262. package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
  263. package/specs/immune-scan-report-v1.schema.json +112 -0
  264. package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
  265. package/specs/memory-atom-v1.schema.json +59 -0
  266. package/specs/memory-event-v1.schema.json +42 -42
  267. package/specs/passport-v1.schema.json +50 -50
  268. package/specs/proof-of-non-use-v1.schema.json +65 -0
  269. package/specs/quarantine-record-v1.schema.json +126 -0
  270. package/specs/receipt-v1.schema.json +61 -61
  271. package/specs/state-checkpoint-v1.schema.json +37 -37
  272. package/specs/trust-bundle-v1.schema.json +56 -56
  273. package/specs/trust-card-v1.schema.json +119 -0
  274. package/docs/proof-network-launch-plan.md +0 -421
  275. package/packages/metering/PACKAGE_CONTRACT.md +0 -20
  276. package/scripts/build-ai-orchestration-plan.mjs +0 -248
@@ -0,0 +1,784 @@
1
+ // sale-witness.test.mjs — tests for zk/sale-witness.mjs, the client-side
2
+ // witness builder for circuits/sealed_sale_release.circom (the ZKCP
3
+ // "Sealed Sale" release proof, private-sale path).
4
+ //
5
+ // Legs:
6
+ // 1. Byte/chunk round-trip — LE 31-byte limbs, every chunk < 2^248 < r;
7
+ // arbitrary bytes (incl. trailing 0x00, 0xFF-filled, 0/1/31/32/992
8
+ // bytes) round-trip exactly; >992 bytes rejected.
9
+ // 2. Poseidon-CTR encrypt/decrypt round-trip + tamper diffusion.
10
+ // 3. Poseidon-CTR KAT — pinned keystream/ciphertext/fold values, plus a
11
+ // fully independent recomputation (raw circomlibjs Poseidon + explicit
12
+ // mod-r field add; no module code).
13
+ // 4. BabyJubJub buyer keys — generated keys validate; encode/decode
14
+ // round-trip; small-order (identity, order-2, order-8) and off-curve
15
+ // and non-canonical points REJECTED with the circuit's exact rule
16
+ // (on-curve ∧ (8·B).x ≠ 0).
17
+ // 5. THIRD-PARTY CANNOT DECRYPT — buyer B's secret on buyer A's envelope
18
+ // yields the wrong DEK; digest assertion fails.
19
+ // 6. saleTermsHash — KAT + independent recomputation; limb/range guards.
20
+ // 7. buildSaleWitness — full witness shape, exact signal names (per the
21
+ // SaleCircuit contract), decimal strings, public-signal order; envelope
22
+ // artifacts self-consistent; client-side rejections throw.
23
+ // 8. Signal-name reconciliation vs circuits/sealed_sale_release.circom
24
+ // (when the file exists): every `signal input` name in the main
25
+ // template must match exactly, public order must match.
26
+ // 9. WASM-gated (polls up to 20 min for SaleCircuit build artifacts;
27
+ // override with SALE_WITNESS_POLL_MS=0 to skip immediately):
28
+ // valid witness satisfies the compiled circuit; a junk-ciphertext
29
+ // witness is REJECTED by the witness calculator.
30
+ //
31
+ // Run: node --test cortex-v3/zk/sale-witness.test.mjs
32
+
33
+ import test from "node:test";
34
+ import assert from "node:assert/strict";
35
+ import { execFileSync } from "node:child_process";
36
+ import { randomBytes } from "node:crypto";
37
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
38
+ import { createRequire } from "node:module";
39
+ import { tmpdir } from "node:os";
40
+ import { dirname, join, resolve } from "node:path";
41
+ import { fileURLToPath } from "node:url";
42
+
43
+ import { buildBabyjub, buildPoseidon } from "circomlibjs";
44
+ import bs58 from "bs58";
45
+
46
+ import { FIELD_R, NoteTree } from "./zk-tree.mjs";
47
+ import {
48
+ CHUNK_BYTES,
49
+ CHUNK_COUNT,
50
+ DOMAIN_SALE,
51
+ NOTE_DEPTH,
52
+ PAYLOAD_CAPACITY,
53
+ SALE_PUBLIC_SIGNALS,
54
+ buildSaleDemoFixture,
55
+ buildSaleWitness,
56
+ bytesToChunks,
57
+ bytesToBigIntLE,
58
+ chunksFromBytes,
59
+ chunksToBytes,
60
+ computeKeyCommit,
61
+ computeSaleNote,
62
+ DOMAIN_PSALT,
63
+ computeSaleTermsHash,
64
+ decryptPayload,
65
+ deriveSharedSecret,
66
+ encodePoint,
67
+ decodePoint,
68
+ encryptPayload,
69
+ foldChunks,
70
+ foldChunksBlind,
71
+ generateBuyerKeyPair,
72
+ keystreamChunk,
73
+ openAndVerifySale,
74
+ openSaleEnvelope,
75
+ validateBuyerPublicKey,
76
+ } from "./sale-witness.mjs";
77
+
78
+ const __dirname = dirname(fileURLToPath(import.meta.url));
79
+ const PKG_ROOT = resolve(__dirname, "..");
80
+ const BUILD = join(PKG_ROOT, "circuits", "build");
81
+ const CIRCUIT_SRC = join(PKG_ROOT, "circuits", "sealed_sale_release.circom");
82
+ const R1CS = join(BUILD, "sealed_sale_release.r1cs");
83
+ const WASM = join(BUILD, "sealed_sale_release_js", "sealed_sale_release.wasm");
84
+ const WITNESS_CALC = join(BUILD, "sealed_sale_release_js", "witness_calculator.js");
85
+
86
+ const require = createRequire(import.meta.url);
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Independent machinery (raw circomlibjs — shares no code with the module)
90
+ // ---------------------------------------------------------------------------
91
+ const poseidonC = await buildPoseidon();
92
+ const F = poseidonC.F;
93
+ const R = FIELD_R;
94
+ const independentPoseidon = (xs) => F.toObject(poseidonC(xs.map((x) => F.e(BigInt(x)))));
95
+
96
+ const babyJub = await buildBabyjub();
97
+ const JF = babyJub.F;
98
+
99
+ /** Independent LE reducer (Buffer-based, no shared code with the module). */
100
+ function leBytes(buf) {
101
+ let v = 0n;
102
+ for (let i = buf.length - 1; i >= 0; i--) v = v * 256n + BigInt(buf[i]);
103
+ return v;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Fixed test fixture
108
+ // ---------------------------------------------------------------------------
109
+ const PLAINTEXT = new TextEncoder().encode(
110
+ "Enigma sealed-sale demo memory: the cortex remembers what the chain never sees.",
111
+ ); // 79 bytes
112
+ const DEK_LO = 0x0de0b6b3a7640000f1f2f3f4f5f6f7f8n;
113
+ const DEK_HI = 0x00112233445566778899aabbccddeeffn;
114
+ const NONCE = 424242424242n;
115
+ const ESK = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403n;
116
+ const OWNER_KEY = 11111111111111111111111111111111111111111111111111n;
117
+ const RHO = 22222222222222222222222222222222222222222222222222n;
118
+ const SELLER = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC mint addr as seller
119
+ const MINT = "So11111111111111111111111111111111111111112"; // wSOL mint
120
+ const PRICE = 2_000_000_000n;
121
+ const EXPIRY_SLOT = 300_000_000n;
122
+
123
+ // Pinned by the independent recomputation below (raw circomlibjs Poseidon,
124
+ // explicit mod-r field add — computed once, then frozen here).
125
+ const KAT = Object.freeze({
126
+ domainSale: 122622912455040968075340901n,
127
+ ks0: 3589178275523291951363275889201219803941083386281678158752858657278878496187n,
128
+ ks1: 14540676613757474377696583013418111239504349028389538056672046070098961572735n,
129
+ ks31: 19323069819120820054275317198827642037090427957986178685749720135057254853371n,
130
+ p0: 103315326233695364645767690488868881140320643153680381253010726472875863621n,
131
+ c0: 3692493601756987316009043579690088685081404029435358540005869383751754359808n,
132
+ payloadCommitPos: 8764819824630054939208652781887457096300910414137815727281144740307113585380n, // DEK-blinded fold
133
+ keyCommit: 12602673196205851515633971187412354398057859532683823173342270131645853115010n,
134
+ });
135
+
136
+ /** Deterministic buyer key pair (fixed secret, < subOrder). */
137
+ function demoBuyer() {
138
+ const secret = 0x0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252n %
139
+ BigInt(babyJub.subOrder.toString());
140
+ const P = babyJub.mulPointEscalar(babyJub.Base8, secret);
141
+ return { secret, publicKey: { x: JF.toObject(P[0]), y: JF.toObject(P[1]) } };
142
+ }
143
+
144
+ /** Build the standard valid fixture: note tree + witness args. */
145
+ function makeFixture() {
146
+ const buyer = demoBuyer();
147
+ const plaintextChunks = bytesToChunks(PLAINTEXT);
148
+ const payloadCommitPos = foldChunksBlind(plaintextChunks, DEK_LO, DEK_HI);
149
+ const noteTree = new NoteTree();
150
+ noteTree.insert(555n); // filler at index 0
151
+ noteTree.insert(computeSaleNote(OWNER_KEY, RHO, payloadCommitPos)); // index 1
152
+ return {
153
+ buyer,
154
+ noteTree,
155
+ args: {
156
+ noteTree,
157
+ noteIndex: 1,
158
+ ownerKey: OWNER_KEY,
159
+ rho: RHO,
160
+ plaintextBytes: PLAINTEXT,
161
+ dekLo: DEK_LO,
162
+ dekHi: DEK_HI,
163
+ nonce: NONCE,
164
+ buyerPub: buyer.publicKey,
165
+ sellerPubkey: SELLER,
166
+ mint: MINT,
167
+ priceLamports: PRICE,
168
+ expirySlot: EXPIRY_SLOT,
169
+ esk: ESK,
170
+ },
171
+ };
172
+ }
173
+
174
+ const EXPECTED_SIGNALS = Object.freeze([
175
+ ...SALE_PUBLIC_SIGNALS,
176
+ "ownerKey",
177
+ "rho",
178
+ "notePathElements",
179
+ "notePathIndices",
180
+ "plaintext",
181
+ "ciphertext",
182
+ "nonce",
183
+ "dekLo",
184
+ "dekHi",
185
+ "buyerPubX",
186
+ "buyerPubY",
187
+ "esk",
188
+ "sellerLo",
189
+ "sellerHi",
190
+ "mintLo",
191
+ "mintHi",
192
+ "price",
193
+ "expirySlot",
194
+ "payloadLen",
195
+ ]);
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // 1. Byte/chunk round-trip
199
+ // ---------------------------------------------------------------------------
200
+ test("bytesToChunks/chunksToBytes round-trip arbitrary bytes", () => {
201
+ const cases = [
202
+ new Uint8Array(0),
203
+ new Uint8Array([0x42]),
204
+ new Uint8Array([0, 0, 0]), // all zeros
205
+ new Uint8Array(randomBytes(1)),
206
+ new Uint8Array(randomBytes(CHUNK_BYTES)), // exactly one chunk
207
+ new Uint8Array(randomBytes(CHUNK_BYTES + 1)), // crosses a chunk boundary
208
+ new Uint8Array(randomBytes(PAYLOAD_CAPACITY)), // full capacity
209
+ (() => {
210
+ const b = new Uint8Array(randomBytes(100));
211
+ b.fill(0, 90); // trailing zero bytes MUST round-trip
212
+ return b;
213
+ })(),
214
+ new Uint8Array(PAYLOAD_CAPACITY).fill(0xff), // max-entropy limbs
215
+ ];
216
+ for (const bytes of cases) {
217
+ const chunks = bytesToChunks(bytes);
218
+ assert.equal(chunks.length, CHUNK_COUNT);
219
+ for (const c of chunks) {
220
+ assert.ok(c >= 0n && c < 1n << 248n, `chunk ${c} outside [0, 2^248)`);
221
+ assert.ok(c < R, `chunk ${c} not canonical mod r`);
222
+ }
223
+ const back = chunksToBytes(chunks, bytes.length);
224
+ assert.deepEqual(back, bytes, `round-trip failed for ${bytes.length} bytes`);
225
+ // aliases must agree
226
+ assert.deepEqual(chunksFromBytes(bytes), chunks);
227
+ }
228
+ // default length = full capacity
229
+ const full = chunksToBytes(bytesToChunks(new Uint8Array([7])));
230
+ assert.equal(full.length, PAYLOAD_CAPACITY);
231
+ assert.equal(full[0], 7);
232
+ assert.ok(full.subarray(1).every((b) => b === 0));
233
+ // over-capacity payload rejected
234
+ assert.throws(() => bytesToChunks(new Uint8Array(PAYLOAD_CAPACITY + 1)), /too large/);
235
+ // out-of-range chunk rejected on decode
236
+ const bad = new Array(CHUNK_COUNT).fill(0n);
237
+ bad[3] = 1n << 248n;
238
+ assert.throws(() => chunksToBytes(bad), /outside \[0, 2\^248\)/);
239
+ });
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // 2. Poseidon-CTR round-trip
243
+ // ---------------------------------------------------------------------------
244
+ test("encryptPayload/decryptPayload round-trip; wrong key/nonce garbles", () => {
245
+ const chunks = bytesToChunks(new Uint8Array(randomBytes(500)));
246
+ const dekLo = bytesToBigIntLE(randomBytes(16));
247
+ const dekHi = bytesToBigIntLE(randomBytes(16));
248
+ const nonce = bytesToBigIntLE(randomBytes(16));
249
+
250
+ const ct = encryptPayload(chunks, dekLo, dekHi, nonce);
251
+ assert.equal(ct.length, CHUNK_COUNT);
252
+ for (const c of ct) assert.ok(c >= 0n && c < R, "ciphertext chunk not canonical");
253
+ assert.notDeepEqual(ct, chunks, "ciphertext must differ from plaintext");
254
+ assert.deepEqual(decryptPayload(ct, dekLo, dekHi, nonce), chunks);
255
+
256
+ // field-add cipher: c_i - p_i === ks_i (mod r) — the circuit constraint
257
+ const ks0 = keystreamChunk(dekLo, dekHi, nonce, 0);
258
+ assert.equal((ct[0] - chunks[0] + R) % R, ks0);
259
+
260
+ // wrong DEK → wrong plaintext → different fold
261
+ const wrong = decryptPayload(ct, dekLo ^ 1n, dekHi, nonce);
262
+ assert.notDeepEqual(wrong, chunks);
263
+ assert.notEqual(foldChunks(wrong), foldChunks(chunks));
264
+ // wrong nonce → same
265
+ const wrongN = decryptPayload(ct, dekLo, dekHi, nonce + 1n);
266
+ assert.notDeepEqual(wrongN, chunks);
267
+ });
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // 3. Poseidon-CTR KAT (pinned + independent recomputation)
271
+ // ---------------------------------------------------------------------------
272
+ test("Poseidon-CTR matches pinned KAT and independent recomputation", () => {
273
+ assert.equal(DOMAIN_SALE, KAT.domainSale, "DOMAIN_SALE drifted");
274
+
275
+ // pinned keystream values
276
+ assert.equal(keystreamChunk(DEK_LO, DEK_HI, NONCE, 0), KAT.ks0);
277
+ assert.equal(keystreamChunk(DEK_LO, DEK_HI, NONCE, 1), KAT.ks1);
278
+ assert.equal(keystreamChunk(DEK_LO, DEK_HI, NONCE, 31), KAT.ks31);
279
+
280
+ // independent keystream recomputation (raw circomlibjs)
281
+ for (const [i, pinned] of [[0n, KAT.ks0], [1n, KAT.ks1], [31n, KAT.ks31]]) {
282
+ assert.equal(
283
+ independentPoseidon([DEK_LO, DEK_HI, NONCE, i]),
284
+ pinned,
285
+ `independent Poseidon4 recomputation disagrees at i=${i}`,
286
+ );
287
+ }
288
+
289
+ // independent chunk derivation + encryption of the fixture plaintext
290
+ const p0 = leBytes(Buffer.from(PLAINTEXT.subarray(0, 31)));
291
+ assert.equal(p0, KAT.p0);
292
+ const c0 = (p0 + independentPoseidon([DEK_LO, DEK_HI, NONCE, 0n])) % R;
293
+ assert.equal(c0, KAT.c0, "independent field-add encryption disagrees");
294
+
295
+ const chunks = bytesToChunks(PLAINTEXT);
296
+ assert.equal(chunks[0], KAT.p0);
297
+ assert.equal(encryptPayload(chunks, DEK_LO, DEK_HI, NONCE)[0], KAT.c0);
298
+
299
+ // fold KAT (payloadCommitPos of the fixture plaintext — DEK-blinded fold)
300
+ let acc = independentPoseidon([DOMAIN_PSALT, DEK_LO, DEK_HI]);
301
+ for (const c of chunks) acc = independentPoseidon([acc, c]);
302
+ assert.equal(acc, KAT.payloadCommitPos);
303
+ assert.equal(foldChunksBlind(chunks, DEK_LO, DEK_HI), KAT.payloadCommitPos);
304
+ // a different DEK seeds a different commitment (dictionary-defense property)
305
+ assert.notEqual(foldChunksBlind(chunks, DEK_LO + 1n, DEK_HI), KAT.payloadCommitPos);
306
+
307
+ // keyCommit KAT
308
+ assert.equal(computeKeyCommit(DEK_LO, DEK_HI), KAT.keyCommit);
309
+ assert.equal(independentPoseidon([DEK_LO, DEK_HI]), KAT.keyCommit);
310
+ });
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // 4. BabyJubJub buyer keys
314
+ // ---------------------------------------------------------------------------
315
+ test("buyer key generation, encode/decode, and validation (circuit-exact rule)", () => {
316
+ const kp = generateBuyerKeyPair();
317
+ assert.ok(kp.secret > 0n && kp.secret < BigInt(babyJub.subOrder.toString()));
318
+ // on-curve per raw circomlibjs
319
+ assert.ok(babyJub.inCurve([JF.e(kp.publicKey.x), JF.e(kp.publicKey.y)]));
320
+ // validateBuyerPublicKey accepts and returns canonical bigints
321
+ assert.deepEqual(validateBuyerPublicKey(kp.publicKey), [kp.publicKey.x, kp.publicKey.y]);
322
+
323
+ // point encode/decode round-trip (decimal strings)
324
+ const enc = encodePoint(kp.publicKey);
325
+ assert.match(enc.x, /^\d+$/);
326
+ assert.match(enc.y, /^\d+$/);
327
+ assert.deepEqual(decodePoint(enc), [kp.publicKey.x, kp.publicKey.y]);
328
+ assert.deepEqual(validateBuyerPublicKey(enc), [kp.publicKey.x, kp.publicKey.y]);
329
+
330
+ // Base8 itself is a valid recipient key
331
+ assert.deepEqual(validateBuyerPublicKey(encodePoint(babyJub.Base8)), [
332
+ JF.toObject(babyJub.Base8[0]),
333
+ JF.toObject(babyJub.Base8[1]),
334
+ ]);
335
+ });
336
+
337
+ test("small-order, off-curve, and non-canonical buyer keys are rejected", () => {
338
+ // identity (0, 1) — order 1
339
+ assert.throws(() => validateBuyerPublicKey({ x: 0n, y: 1n }), /small-order/);
340
+ // order-2 point (0, -1) = (0, r-1)
341
+ assert.throws(() => validateBuyerPublicKey({ x: 0n, y: R - 1n }), /small-order/);
342
+ // order-8 point: subOrder · Generator (Generator spans the full group, so
343
+ // multiplying by the prime cofactor quotient lands in the 8-torsion; it has
344
+ // x ≠ 0, so this exercises the 8·B x-check and not the on-curve equation).
345
+ const torsion = babyJub.mulPointEscalar(babyJub.Generator, babyJub.subOrder);
346
+ const t8 = babyJub.mulPointEscalar(torsion, 8n);
347
+ assert.ok(JF.isZero(t8[0]), "test premise: chosen point must be 8-torsion");
348
+ assert.ok(babyJub.inCurve(torsion));
349
+ assert.throws(
350
+ () => validateBuyerPublicKey({ x: JF.toObject(torsion[0]), y: JF.toObject(torsion[1]) }),
351
+ /small-order/,
352
+ );
353
+ // off-curve point
354
+ assert.throws(() => validateBuyerPublicKey({ x: 1n, y: 1n }), /not on the BabyJubJub curve/);
355
+ // non-canonical coordinates (>= r) — must never reach the curve check
356
+ assert.throws(() => validateBuyerPublicKey({ x: R, y: 1n }), /canonical/);
357
+ assert.throws(() => validateBuyerPublicKey({ x: 1n, y: R + 5n }), /canonical/);
358
+ // malformed shape
359
+ assert.throws(() => validateBuyerPublicKey({}), /expected \{x, y\}/);
360
+ });
361
+
362
+ // ---------------------------------------------------------------------------
363
+ // 5. Third-party cannot decrypt
364
+ // ---------------------------------------------------------------------------
365
+ test("third party cannot decrypt: wrong buyer secret fails the digest assert", () => {
366
+ const { args, buyer } = makeFixture();
367
+ const result = buildSaleWitness(args);
368
+
369
+ // intended buyer opens the envelope and the digest checks out
370
+ const opened = openAndVerifySale({
371
+ buyerSecret: buyer.secret,
372
+ epk: result.epk,
373
+ encDEK_lo: result.encDEK_lo,
374
+ encDEK_hi: result.encDEK_hi,
375
+ ciphertext: result.ciphertextChunks,
376
+ cipherCommit: result.cipherCommit,
377
+ nonce: NONCE,
378
+ payloadCommitPos: result.payloadCommitPos,
379
+ });
380
+ assert.equal(opened.cipherCommit, result.cipherCommit);
381
+ assert.equal(opened.dekLo, DEK_LO);
382
+ assert.equal(opened.dekHi, DEK_HI);
383
+ assert.deepEqual(opened.plaintextChunks, result.plaintextChunks);
384
+
385
+ // a third party (buyer B) derives a different shared secret → wrong DEK
386
+ const mallory = generateBuyerKeyPair();
387
+ const stolen = openSaleEnvelope(
388
+ mallory.secret,
389
+ result.epk,
390
+ result.encDEK_lo,
391
+ result.encDEK_hi,
392
+ );
393
+ assert.notEqual(stolen.dekLo, DEK_LO, "third party must NOT recover the DEK");
394
+ assert.notEqual(stolen.dekHi, DEK_HI);
395
+
396
+ // ...and the digest assertion rejects the garbage plaintext
397
+ assert.throws(
398
+ () =>
399
+ openAndVerifySale({
400
+ buyerSecret: mallory.secret,
401
+ epk: result.epk,
402
+ encDEK_lo: result.encDEK_lo,
403
+ encDEK_hi: result.encDEK_hi,
404
+ ciphertext: result.ciphertextChunks,
405
+ cipherCommit: result.cipherCommit,
406
+ nonce: NONCE,
407
+ payloadCommitPos: result.payloadCommitPos,
408
+ }),
409
+ /digest mismatch/,
410
+ );
411
+
412
+ // junk delivery: ciphertext that does NOT fold to the public cipherCommit
413
+ // is rejected before any decryption — this is the pre-payment defense.
414
+ const junk = [...result.ciphertextChunks];
415
+ junk[0] = (junk[0] + 1n) % R;
416
+ assert.throws(
417
+ () =>
418
+ openAndVerifySale({
419
+ buyerSecret: buyer.secret,
420
+ epk: result.epk,
421
+ encDEK_lo: result.encDEK_lo,
422
+ encDEK_hi: result.encDEK_hi,
423
+ ciphertext: junk,
424
+ cipherCommit: result.cipherCommit,
425
+ nonce: NONCE,
426
+ payloadCommitPos: result.payloadCommitPos,
427
+ }),
428
+ /junk delivery/,
429
+ );
430
+ // cipherCommit is mandatory (it is the public, on-chain commitment)
431
+ assert.throws(
432
+ () =>
433
+ openAndVerifySale({
434
+ buyerSecret: buyer.secret,
435
+ epk: result.epk,
436
+ encDEK_lo: result.encDEK_lo,
437
+ encDEK_hi: result.encDEK_hi,
438
+ ciphertext: result.ciphertextChunks,
439
+ nonce: NONCE,
440
+ payloadCommitPos: result.payloadCommitPos,
441
+ }),
442
+ /missing cipherCommit/,
443
+ );
444
+
445
+ // sanity: both parties agree on ss only when the secret matches
446
+ const sellerSide = deriveSharedSecret(ESK, buyer.publicKey);
447
+ const buyerSide = deriveSharedSecret(buyer.secret, result.epk);
448
+ assert.equal(sellerSide.ss, buyerSide.ss, "ECDH must agree for the intended pair");
449
+ const mallorySide = deriveSharedSecret(mallory.secret, result.epk);
450
+ assert.notEqual(mallorySide.ss, buyerSide.ss);
451
+ });
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // 6. saleTermsHash
455
+ // ---------------------------------------------------------------------------
456
+ test("computeSaleTermsHash matches independent recomputation; guards ranges", () => {
457
+ const buyer = demoBuyer();
458
+ const out = computeSaleTermsHash({
459
+ sellerPubkey: SELLER,
460
+ buyerPub: buyer.publicKey,
461
+ mint: MINT,
462
+ payloadLen: 79n,
463
+ priceLamports: PRICE,
464
+ expirySlot: EXPIRY_SLOT,
465
+ });
466
+ const s = bs58.decode(SELLER);
467
+ const m = bs58.decode(MINT);
468
+ assert.equal(out.sellerLo, leBytes(s.subarray(0, 16)));
469
+ assert.equal(out.sellerHi, leBytes(s.subarray(16, 32)));
470
+ assert.equal(out.mintLo, leBytes(m.subarray(0, 16)));
471
+ assert.equal(out.mintHi, leBytes(m.subarray(16, 32)));
472
+ for (const limb of [out.sellerLo, out.sellerHi, out.mintLo, out.mintHi]) {
473
+ assert.ok(limb < 1n << 128n);
474
+ }
475
+ assert.equal(out.buyerPubX, buyer.publicKey.x);
476
+ assert.equal(out.buyerPubY, buyer.publicKey.y);
477
+ // Poseidon10: buyer pubkey limbs sit between sellerHi and mintLo; payloadLen last
478
+ assert.equal(
479
+ out.saleTermsHash,
480
+ independentPoseidon([
481
+ DOMAIN_SALE,
482
+ out.sellerLo,
483
+ out.sellerHi,
484
+ buyer.publicKey.x,
485
+ buyer.publicKey.y,
486
+ out.mintLo,
487
+ out.mintHi,
488
+ PRICE,
489
+ EXPIRY_SLOT,
490
+ 79n,
491
+ ]),
492
+ );
493
+
494
+ // collusion defense: a different buyer key MUST change the terms hash, so a
495
+ // receipt wrapped to a colluder can never match an honest buyer's escrow.
496
+ const colluder = generateBuyerKeyPair();
497
+ const colluderTerms = computeSaleTermsHash({
498
+ sellerPubkey: SELLER,
499
+ buyerPub: colluder.publicKey,
500
+ mint: MINT,
501
+ payloadLen: 79n,
502
+ priceLamports: PRICE,
503
+ expirySlot: EXPIRY_SLOT,
504
+ });
505
+ assert.notEqual(colluderTerms.saleTermsHash, out.saleTermsHash);
506
+
507
+ assert.throws(
508
+ () => computeSaleTermsHash({ sellerPubkey: "1111", buyerPub: buyer.publicKey, mint: MINT, priceLamports: PRICE, expirySlot: EXPIRY_SLOT, payloadLen: 79n }),
509
+ /32 bytes/,
510
+ );
511
+ assert.throws(
512
+ () => computeSaleTermsHash({ sellerPubkey: SELLER, buyerPub: { x: 0n, y: 1n }, mint: MINT, priceLamports: PRICE, expirySlot: EXPIRY_SLOT, payloadLen: 79n }),
513
+ /small-order/,
514
+ );
515
+ assert.throws(
516
+ () => computeSaleTermsHash({ sellerPubkey: SELLER, buyerPub: buyer.publicKey, mint: MINT, priceLamports: 1n << 64n, expirySlot: EXPIRY_SLOT, payloadLen: 79n }),
517
+ /u64/,
518
+ );
519
+ assert.throws(
520
+ () => computeSaleTermsHash({ sellerPubkey: SELLER, buyerPub: buyer.publicKey, mint: MINT, priceLamports: PRICE, expirySlot: 1n << 48n, payloadLen: 79n }),
521
+ /48-bit/,
522
+ );
523
+ });
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // 7. buildSaleWitness
527
+ // ---------------------------------------------------------------------------
528
+ test("buildSaleWitness emits exact signal names, decimal strings, public order", () => {
529
+ const { args, noteTree, buyer } = makeFixture();
530
+ const r = buildSaleWitness(args);
531
+ const w = r.input;
532
+
533
+ assert.deepEqual(
534
+ Object.keys(w).sort(),
535
+ [...EXPECTED_SIGNALS].sort(),
536
+ "witness signal-name set drifted from the circuit spec",
537
+ );
538
+ for (const [k, v] of Object.entries(w)) {
539
+ if (Array.isArray(v)) {
540
+ for (const x of v) assert.match(x, /^\d+$/, `${k} element not a decimal string`);
541
+ } else {
542
+ assert.match(v, /^\d+$/, `${k} not a decimal string`);
543
+ }
544
+ }
545
+
546
+ assert.equal(w.notePathElements.length, NOTE_DEPTH);
547
+ assert.equal(w.notePathIndices.length, NOTE_DEPTH);
548
+ assert.equal(w.plaintext.length, CHUNK_COUNT);
549
+ assert.equal(w.ciphertext.length, CHUNK_COUNT);
550
+
551
+ // public values (exact order contract — 6 publics, cipherCommit at index 2)
552
+ assert.deepEqual(
553
+ SALE_PUBLIC_SIGNALS,
554
+ ["noteRoot", "payloadCommitPos", "cipherCommit", "keyCommit", "saleTermsHash", "envelopeCommit"],
555
+ );
556
+ assert.equal(w.cipherCommit, foldChunks(r.ciphertextChunks).toString());
557
+ assert.equal(w.noteRoot, noteTree.root().toString());
558
+ assert.equal(w.payloadCommitPos, KAT.payloadCommitPos.toString());
559
+ assert.equal(w.keyCommit, KAT.keyCommit.toString());
560
+ assert.equal(
561
+ w.saleTermsHash,
562
+ computeSaleTermsHash({
563
+ sellerPubkey: SELLER,
564
+ buyerPub: buyer.publicKey,
565
+ mint: MINT,
566
+ payloadLen: 79n,
567
+ priceLamports: PRICE,
568
+ expirySlot: EXPIRY_SLOT,
569
+ }).saleTermsHash.toString(),
570
+ );
571
+ // buyer-bound terms: the witness buyerPubX/Y feed the terms hash
572
+ assert.equal(w.saleTermsHash, r.saleTermsHash.toString());
573
+ assert.equal(r.buyerPubX, buyer.publicKey.x);
574
+ assert.equal(r.buyerPubY, buyer.publicKey.y);
575
+
576
+ // envelope self-consistency (independent recomputation from raw signals)
577
+ const epkP = babyJub.mulPointEscalar(babyJub.Base8, ESK);
578
+ assert.deepEqual(r.epk, { x: JF.toObject(epkP[0]), y: JF.toObject(epkP[1]) });
579
+ const sharedP = babyJub.mulPointEscalar([JF.e(buyer.publicKey.x), JF.e(buyer.publicKey.y)], ESK);
580
+ const ss = independentPoseidon([JF.toObject(sharedP[0]), JF.toObject(sharedP[1])]);
581
+ assert.equal(r.encDEK_lo, (DEK_LO + independentPoseidon([ss, 0n])) % R);
582
+ assert.equal(r.encDEK_hi, (DEK_HI + independentPoseidon([ss, 1n])) % R);
583
+ assert.equal(
584
+ r.envelopeCommit,
585
+ independentPoseidon([r.epk.x, r.epk.y, r.encDEK_lo, r.encDEK_hi, NONCE]),
586
+ );
587
+ assert.equal(w.envelopeCommit, r.envelopeCommit.toString());
588
+
589
+ // private consistency
590
+ assert.equal(w.dekLo, DEK_LO.toString());
591
+ assert.equal(w.dekHi, DEK_HI.toString());
592
+ assert.equal(w.nonce, NONCE.toString());
593
+ assert.equal(w.esk, ESK.toString());
594
+ assert.equal(w.buyerPubX, buyer.publicKey.x.toString());
595
+ assert.equal(w.buyerPubY, buyer.publicKey.y.toString());
596
+ assert.equal(w.cipherCommit, foldChunks(r.ciphertextChunks).toString());
597
+ assert.equal(BigInt(w.plaintext[0]), KAT.p0);
598
+ assert.equal(BigInt(w.ciphertext[0]), KAT.c0);
599
+ });
600
+
601
+ test("buildSaleWitness rejects malformed sales client-side", () => {
602
+ const { args } = makeFixture();
603
+ // note opening mismatch
604
+ assert.throws(() => buildSaleWitness({ ...args, rho: RHO + 1n }), /does not match the leaf/);
605
+ // stale noteIndex
606
+ assert.throws(() => buildSaleWitness({ ...args, noteIndex: 9 }), /not present/);
607
+ // DEK limb overflow
608
+ assert.throws(() => buildSaleWitness({ ...args, dekLo: 1n << 128n }), /2\^128/);
609
+ // small-order buyer key
610
+ assert.throws(() => buildSaleWitness({ ...args, buyerPub: { x: 0n, y: 1n } }), /small-order/);
611
+ // off-curve buyer key
612
+ assert.throws(() => buildSaleWitness({ ...args, buyerPub: { x: 1n, y: 1n } }), /not on/);
613
+ // esk out of Num2Bits(253) range
614
+ assert.throws(() => buildSaleWitness({ ...args, esk: 1n << 253n }), /253/);
615
+ // oversize plaintext
616
+ assert.throws(
617
+ () => buildSaleWitness({ ...args, plaintextBytes: new Uint8Array(PAYLOAD_CAPACITY + 1) }),
618
+ /out of range|too large/,
619
+ );
620
+ // random esk path (no esk supplied) still yields a valid witness
621
+ const { args: args2 } = makeFixture();
622
+ delete args2.esk;
623
+ const r2 = buildSaleWitness(args2);
624
+ assert.ok(BigInt(r2.input.esk) < 1n << 253n);
625
+ });
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // 7b. Demo fixture contract (consumed by zk/setup-sale.mjs proof-of-life)
629
+ // ---------------------------------------------------------------------------
630
+ test("buildSaleDemoFixture returns input + expectedPublic in circuit order", async () => {
631
+ const fx = await buildSaleDemoFixture();
632
+ assert.deepEqual(Object.keys(fx.input).sort(), [...EXPECTED_SIGNALS].sort());
633
+ assert.equal(fx.expectedPublic.length, 6);
634
+ assert.deepEqual(
635
+ fx.expectedPublic,
636
+ SALE_PUBLIC_SIGNALS.map((n) => fx.input[n]),
637
+ );
638
+ for (const v of fx.expectedPublic) assert.match(v, /^\d+$/);
639
+ // buyer can open the demo envelope
640
+ const opened = openAndVerifySale({
641
+ buyerSecret: fx.buyerSecret,
642
+ epk: fx.artifacts.epk,
643
+ encDEK_lo: fx.artifacts.encDEK_lo,
644
+ encDEK_hi: fx.artifacts.encDEK_hi,
645
+ ciphertext: fx.artifacts.ciphertextChunks,
646
+ cipherCommit: fx.artifacts.cipherCommit,
647
+ nonce: fx.input.nonce,
648
+ payloadCommitPos: fx.artifacts.payloadCommitPos,
649
+ });
650
+ assert.deepEqual(
651
+ chunksToBytes(opened.plaintextChunks, fx.plaintextBytes.length),
652
+ fx.plaintextBytes,
653
+ );
654
+ });
655
+
656
+ // ---------------------------------------------------------------------------
657
+ // 8. Signal-name reconciliation vs the .circom source (when present)
658
+ // ---------------------------------------------------------------------------
659
+ test("signal names reconcile with circuits/sealed_sale_release.circom", (t) => {
660
+ if (!existsSync(CIRCUIT_SRC)) {
661
+ t.skip("circuits/sealed_sale_release.circom not present yet (SaleCircuit in flight)");
662
+ return;
663
+ }
664
+ const src = readFileSync(CIRCUIT_SRC, "utf8").replace(/\/\/[^\n]*/g, "");
665
+ // Scope to the main template body: helper templates (DualMux/PoseidonN/
666
+ // chunk fold) declare their own generic `signal input`s that are not part
667
+ // of the witness contract. The main template is the last one before
668
+ // `component main`.
669
+ const mainIdx = src.indexOf("component main");
670
+ assert.ok(mainIdx !== -1, "component main not found");
671
+ const tplStart = src.lastIndexOf("template ", mainIdx);
672
+ assert.ok(tplStart !== -1, "main template not found");
673
+ const tpl = src.slice(tplStart, mainIdx);
674
+ const declared = new Set();
675
+ for (const m of tpl.matchAll(/signal\s+input\s+(\w+)/g)) declared.add(m[1]);
676
+ assert.ok(declared.size > 0, "no `signal input` declarations found in the circuit");
677
+ assert.deepEqual(
678
+ [...declared].sort(),
679
+ [...EXPECTED_SIGNALS].sort(),
680
+ "circuit signal inputs diverge from sale-witness.mjs emission",
681
+ );
682
+ // public order in component main
683
+ const mainMatch = src.match(/component\s+main\s*\{[^}]*public\s*\[([^\]]*)\]/s);
684
+ assert.ok(mainMatch, "component main {public [...]} not found");
685
+ const pub = mainMatch[1].split(",").map((s) => s.trim());
686
+ assert.deepEqual(pub, [...SALE_PUBLIC_SIGNALS], "public input order diverges");
687
+ });
688
+
689
+ // ---------------------------------------------------------------------------
690
+ // 9. WASM-gated circuit satisfaction legs
691
+ // ---------------------------------------------------------------------------
692
+ const POLL_MS = Number(process.env.SALE_WITNESS_POLL_MS ?? 20 * 60 * 1000);
693
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
694
+
695
+ let wasmReady = existsSync(WASM) && existsSync(WITNESS_CALC);
696
+ if (!wasmReady && POLL_MS > 0) {
697
+ const deadline = Date.now() + POLL_MS;
698
+ while (Date.now() < deadline) {
699
+ await sleep(15_000);
700
+ if (existsSync(WASM) && existsSync(WITNESS_CALC)) {
701
+ wasmReady = true;
702
+ break;
703
+ }
704
+ }
705
+ }
706
+ if (!wasmReady) {
707
+ console.log(
708
+ `[sale-witness] sealed_sale_release wasm artifacts absent after polling ` +
709
+ `(${WASM}); circuit-satisfaction legs will SKIP.`,
710
+ );
711
+ }
712
+
713
+ /** `snarkjs wtns check` → true if the witness satisfies the R1CS. */
714
+ function wtnsCheck(wtnsPath) {
715
+ try {
716
+ execFileSync("npx", ["snarkjs", "wtns", "check", R1CS, wtnsPath], {
717
+ shell: true,
718
+ stdio: "pipe",
719
+ cwd: PKG_ROOT,
720
+ });
721
+ return true;
722
+ } catch {
723
+ return false;
724
+ }
725
+ }
726
+
727
+ test("compiled circuit accepts a valid sale witness", async (t) => {
728
+ if (!wasmReady) {
729
+ t.skip("sealed_sale_release wasm absent (see log above); nothing to check against");
730
+ return;
731
+ }
732
+ const tmp = mkdtempSync(join(tmpdir(), "enigma-sale-"));
733
+ t.after(() => rmSync(tmp, { recursive: true, force: true }));
734
+
735
+ const { args } = makeFixture();
736
+ const { input } = buildSaleWitness(args);
737
+
738
+ const buildCalc = require(WITNESS_CALC);
739
+ const calc = await buildCalc(readFileSync(WASM));
740
+ // sanityCheck=true: the calculator enforces every constraint (asserts).
741
+ const bin = Buffer.from(await calc.calculateWTNSBin(input, true));
742
+ const wtnsPath = join(tmp, "sale.wtns");
743
+ writeFileSync(wtnsPath, bin);
744
+
745
+ if (existsSync(R1CS)) {
746
+ assert.ok(wtnsCheck(wtnsPath), "valid sale witness must satisfy the R1CS");
747
+ }
748
+ });
749
+
750
+ test("compiled circuit rejects a junk-ciphertext witness", async (t) => {
751
+ if (!wasmReady) {
752
+ t.skip("sealed_sale_release wasm absent (see log above); nothing to check against");
753
+ return;
754
+ }
755
+ const { args } = makeFixture();
756
+ const { input } = buildSaleWitness(args);
757
+ const buildCalc = require(WITNESS_CALC);
758
+ const calc = await buildCalc(readFileSync(WASM));
759
+
760
+ // Tamper one ciphertext chunk WITHOUT touching the plaintext: the circuit
761
+ // constraint c_i - ks_i === p_i (and the cipherCommit fold) must fail —
762
+ // exactly what a seller would try if delivering junk bytes while claiming
763
+ // the listed payloadCommitPos.
764
+ const junk = { ...input };
765
+ junk.ciphertext = [...input.ciphertext];
766
+ junk.ciphertext[0] = ((BigInt(input.ciphertext[0]) + 1n) % R).toString();
767
+ await assert.rejects(
768
+ () => calc.calculateWTNSBin(junk, true),
769
+ /Assert Failed|Error/,
770
+ "circuit must reject ciphertext inconsistent with the committed plaintext",
771
+ );
772
+
773
+ // And the honest plaintext fold bound into the note must match the listed
774
+ // payloadCommitPos: tampering the plaintext breaks BOTH the note opening
775
+ // and the encryption-consistency legs.
776
+ const junkPt = { ...input };
777
+ junkPt.plaintext = [...input.plaintext];
778
+ junkPt.plaintext[0] = ((BigInt(input.plaintext[0]) + 1n) % R).toString();
779
+ await assert.rejects(
780
+ () => calc.calculateWTNSBin(junkPt, true),
781
+ /Assert Failed|Error/,
782
+ "circuit must reject plaintext inconsistent with the note/ciphertext",
783
+ );
784
+ });