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,256 @@
1
+ // zk-tree.mjs — Poseidon Merkle trees for the Enigma per-action ZK circuit.
2
+ // Implements the shared conventions of cortex-v3/zk/BUILD-CONTRACT.md v3:
3
+ // - BN254 Poseidon (circomlibjs), field
4
+ // r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
5
+ // - zeros[0] = 0, zeros[i+1] = Poseidon([zeros[i], zeros[i]])
6
+ // - path index bits are LSB-first (bit at level l = (index >> l) & 1)
7
+ // - field elements are bigint everywhere in JS; decimal-string JSON emission
8
+ // happens in witness.mjs.
9
+ //
10
+ // NoteTree: append-only incremental Merkle tree (D=20), shared note tree.
11
+ // NullifierIMT: Aztec-style indexed Merkle tree (D=20) over
12
+ // { value, nextIndex, nextValue } leaves hashed Poseidon([value, nextIndex, nextValue]),
13
+ // genesis leaf index 0 = { 0, 0, r - 1 } (sentinel infinity = r - 1).
14
+
15
+ import { buildPoseidon } from "circomlibjs";
16
+
17
+ const poseidonC = await buildPoseidon();
18
+ const F = poseidonC.F;
19
+
20
+ /** BN254 scalar field order r. */
21
+ export const FIELD_R = F.p;
22
+ /** Tree depth fixed by the circuit (ActionTransition(D=20)). */
23
+ export const TREE_DEPTH = 20;
24
+ /** Maximum number of leaves in a D=20 tree. */
25
+ export const MAX_LEAVES = 2 ** TREE_DEPTH;
26
+
27
+ /**
28
+ * DOMAIN_REVOCATION = 0x656e69676d615f7265766f6b65 ("enigma_revoke" ASCII),
29
+ * decimal 8036215190653564882703377984357. Epoch-independent, deterministic
30
+ * nullifier domain separator: nf = Poseidon([DOMAIN_REVOCATION, ownerKey, rho]).
31
+ */
32
+ export const DOMAIN_REVOCATION = BigInt("0x656e69676d615f7265766f6b65");
33
+
34
+ /** Poseidon hash over an array of bigint field elements → bigint. */
35
+ export function poseidon(inputs) {
36
+ return F.toObject(poseidonC(inputs.map((x) => F.e(BigInt(x)))));
37
+ }
38
+
39
+ /** Zero chain: zeros[0] = 0n, zeros[i+1] = Poseidon([zeros[i], zeros[i]]). */
40
+ export function buildZeros(depth = TREE_DEPTH) {
41
+ const zeros = [0n];
42
+ for (let i = 0; i < depth; i++) zeros.push(poseidon([zeros[i], zeros[i]]));
43
+ return zeros;
44
+ }
45
+
46
+ /** Shared zero chain for D=20. */
47
+ export const ZEROS = buildZeros(TREE_DEPTH);
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Shared sparse-Merkle machinery (append-only: only the filled prefix exists,
51
+ // every node past leafHashes.length at level l is zeros[l]).
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /** Root of the append-only Merkle tree whose filled leaves are `leafHashes`. */
55
+ export function computeRoot(leafHashes, zeros = ZEROS, depth = TREE_DEPTH) {
56
+ if (leafHashes.length === 0) return zeros[depth];
57
+ let level = leafHashes.slice();
58
+ for (let l = 0; l < depth; l++) {
59
+ const next = [];
60
+ for (let i = 0; i < level.length; i += 2) {
61
+ const right = i + 1 < level.length ? level[i + 1] : zeros[l];
62
+ next.push(poseidon([level[i], right]));
63
+ }
64
+ level = next;
65
+ }
66
+ return level[0];
67
+ }
68
+
69
+ /**
70
+ * Merkle proof for `index` (filled or not) against the current state.
71
+ * Returns { pathElements, pathIndices } with LSB-first index bits.
72
+ */
73
+ export function computeProof(leafHashes, index, zeros = ZEROS, depth = TREE_DEPTH) {
74
+ if (!Number.isInteger(index) || index < 0 || index >= 2 ** depth) {
75
+ throw new RangeError(`leaf index ${index} out of range for depth ${depth}`);
76
+ }
77
+ const pathElements = [];
78
+ const pathIndices = [];
79
+ let level = leafHashes.slice();
80
+ let idx = index;
81
+ for (let l = 0; l < depth; l++) {
82
+ const sib = idx ^ 1;
83
+ pathElements.push(sib < level.length ? level[sib] : zeros[l]);
84
+ pathIndices.push(idx & 1);
85
+ const next = [];
86
+ for (let i = 0; i < level.length; i += 2) {
87
+ const right = i + 1 < level.length ? level[i + 1] : zeros[l];
88
+ next.push(poseidon([level[i], right]));
89
+ }
90
+ level = next;
91
+ idx >>= 1;
92
+ }
93
+ return { pathElements, pathIndices };
94
+ }
95
+
96
+ /** Fold a proof: leaf + LSB-first bits + siblings → root equality check. */
97
+ export function verifyMerkleProof(leaf, pathElements, pathIndices, root, depth = TREE_DEPTH) {
98
+ if (pathElements.length !== depth || pathIndices.length !== depth) return false;
99
+ let node = BigInt(leaf);
100
+ for (let l = 0; l < depth; l++) {
101
+ const bit = pathIndices[l];
102
+ if (bit !== 0 && bit !== 1) return false;
103
+ node = bit === 1 ? poseidon([pathElements[l], node]) : poseidon([node, pathElements[l]]);
104
+ }
105
+ return node === BigInt(root);
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // NoteTree — append-only incremental Merkle tree of Poseidon notes (D=20).
110
+ // ---------------------------------------------------------------------------
111
+
112
+ export class NoteTree {
113
+ constructor(depth = TREE_DEPTH, leaves = []) {
114
+ this.depth = depth;
115
+ this.zeros = buildZeros(depth);
116
+ this.leaves = leaves.map((l) => BigInt(l));
117
+ }
118
+
119
+ /** Non-mutating copy (used to snapshot pre-action state). */
120
+ clone() {
121
+ return new NoteTree(this.depth, this.leaves);
122
+ }
123
+
124
+ /** Index the next insert will occupy. */
125
+ get nextIndex() {
126
+ return this.leaves.length;
127
+ }
128
+
129
+ root() {
130
+ return computeRoot(this.leaves, this.zeros, this.depth);
131
+ }
132
+
133
+ /** Proof for any index (filled leaf or zero slot) against the current root. */
134
+ getProof(index) {
135
+ return computeProof(this.leaves, index, this.zeros, this.depth);
136
+ }
137
+
138
+ /**
139
+ * Append `leaf`. Returns { index, pathElements, pathIndices } — the proof of
140
+ * the PRE-insert zero leaf (zeros[0] = 0) at `index` against the pre-insert
141
+ * root, exactly what the APPEND mode of the circuit consumes.
142
+ */
143
+ insert(leaf) {
144
+ const index = this.leaves.length;
145
+ if (index >= 2 ** this.depth) throw new RangeError("note tree full");
146
+ const { pathElements, pathIndices } = this.getProof(index);
147
+ this.leaves.push(BigInt(leaf));
148
+ return { index, pathElements, pathIndices };
149
+ }
150
+
151
+ verifyProof(leaf, pathElements, pathIndices, root = this.root()) {
152
+ return verifyMerkleProof(leaf, pathElements, pathIndices, root, this.depth);
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // NullifierIMT — indexed Merkle tree (D=20) for nullifier non-membership.
158
+ // Leaves { value, nextIndex, nextValue }, hashed Poseidon([value, nextIndex, nextValue]).
159
+ // Sorted linked list: leaf.next* points at the leaf with the next larger value.
160
+ // Genesis: index 0 = { 0n, 0n, r - 1n }.
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export class NullifierIMT {
164
+ constructor(depth = TREE_DEPTH) {
165
+ this.depth = depth;
166
+ this.zeros = buildZeros(depth);
167
+ this.leaves = [{ value: 0n, nextIndex: 0n, nextValue: FIELD_R - 1n }];
168
+ }
169
+
170
+ /** Next append index == on-chain null_count (genesis leaf counts as index 0). */
171
+ get appendCounter() {
172
+ return this.leaves.length;
173
+ }
174
+
175
+ leafHash(leaf) {
176
+ return poseidon([leaf.value, leaf.nextIndex, leaf.nextValue]);
177
+ }
178
+
179
+ leafHashes() {
180
+ return this.leaves.map((l) => this.leafHash(l));
181
+ }
182
+
183
+ root() {
184
+ return computeRoot(this.leafHashes(), this.zeros, this.depth);
185
+ }
186
+
187
+ /** Proof for any index (filled leaf or zero slot) against the current root. */
188
+ getProof(index) {
189
+ return computeProof(this.leafHashes(), index, this.zeros, this.depth);
190
+ }
191
+
192
+ /** Largest stored value strictly < nf. Throws if none (nf <= 0). */
193
+ findPredecessor(nf) {
194
+ nf = BigInt(nf);
195
+ let best = -1;
196
+ for (let i = 0; i < this.leaves.length; i++) {
197
+ const v = this.leaves[i].value;
198
+ if (v < nf && (best === -1 || v > this.leaves[best].value)) best = i;
199
+ }
200
+ if (best === -1) throw new Error(`no predecessor for nullifier ${nf}`);
201
+ return { index: best, leaf: { ...this.leaves[best] } };
202
+ }
203
+
204
+ /**
205
+ * IMT non-membership insert of `nf` (contract v3 REVOKE step):
206
+ * 1. predecessor = largest value < nf; require strict nf < pred.nextValue
207
+ * (rejects duplicates: if nf is already a leaf value, its predecessor's
208
+ * nextValue === nf and the strict inequality fails);
209
+ * 2. predecessorPath = proof of the OLD predecessor leaf vs current root;
210
+ * 3. predecessor updated in place → { predValue, newLeafIndex, nf };
211
+ * 4. appendPath = proof of the zero slot at newLeafIndex vs the updated
212
+ * (intermediate) root;
213
+ * 5. append { nf, oldPredNextIndex, oldPredNextValue } at newLeafIndex.
214
+ * Returns { predecessorPath, predecessorLeaf (OLD), newLeafIndex, appendPath }.
215
+ */
216
+ insert(nf) {
217
+ nf = BigInt(nf);
218
+ if (nf <= 0n || nf >= FIELD_R) {
219
+ throw new RangeError(`nullifier ${nf} out of field range (0, r)`);
220
+ }
221
+ const pred = this.findPredecessor(nf);
222
+ if (!(nf < pred.leaf.nextValue)) {
223
+ throw new Error(
224
+ `nullifier ${nf} violates strict inequality (duplicate or unsorted predecessor)`,
225
+ );
226
+ }
227
+ const predecessorLeaf = pred.leaf; // OLD values { value, nextIndex, nextValue }
228
+ const predecessorPath = this.getProof(pred.index);
229
+ const newLeafIndex = this.leaves.length;
230
+ // 1. predecessor update → intermediate root
231
+ this.leaves[pred.index] = {
232
+ value: predecessorLeaf.value,
233
+ nextIndex: BigInt(newLeafIndex),
234
+ nextValue: nf,
235
+ };
236
+ // 2. zero-leaf proof at newLeafIndex against the intermediate root
237
+ const appendPath = this.getProof(newLeafIndex);
238
+ // 3. append new leaf inheriting the predecessor's OLD next-pointer
239
+ this.leaves.push({
240
+ value: nf,
241
+ nextIndex: predecessorLeaf.nextIndex,
242
+ nextValue: predecessorLeaf.nextValue,
243
+ });
244
+ return { predecessorPath, predecessorLeaf, newLeafIndex, appendPath };
245
+ }
246
+
247
+ /** `leaf` may be a { value, nextIndex, nextValue } object or a raw leaf hash. */
248
+ verifyProof(leaf, pathElements, pathIndices, root = this.root()) {
249
+ const h = typeof leaf === "object" && leaf !== null ? this.leafHash(leaf) : BigInt(leaf);
250
+ return verifyMerkleProof(h, pathElements, pathIndices, root, this.depth);
251
+ }
252
+
253
+ contains(nf) {
254
+ return this.leaves.some((l) => l.value === BigInt(nf));
255
+ }
256
+ }
@@ -0,0 +1,419 @@
1
+ // zk-tree.test.mjs — node:test suite for zk-tree.mjs + witness.mjs
2
+ // (BUILD-CONTRACT.md v3: sovereign per-action proofs, ActionTransition(D=20)).
3
+ // Run: node --test cortex-v3/zk/zk-tree.test.mjs
4
+
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import {
8
+ DOMAIN_REVOCATION,
9
+ FIELD_R,
10
+ NoteTree,
11
+ NullifierIMT,
12
+ TREE_DEPTH,
13
+ ZEROS,
14
+ poseidon,
15
+ verifyMerkleProof,
16
+ } from "./zk-tree.mjs";
17
+ import { buildActionWitness } from "./witness.mjs";
18
+
19
+ const D = TREE_DEPTH;
20
+
21
+ // Fixed sample openings (deterministic tests, arbitrary field elements).
22
+ const OWNER_KEY = 1111111111111111111111111111111111111111111111111111111111111111n;
23
+ const RHO = 2222222222222222222222222222222222222222222222222222222222222222n;
24
+ const PAYLOAD = 3333333333333333333333333333333333333333333333333333333333333333n;
25
+ const OWNER_KEY2 = 4444444444444444444444444444444444444444444444444444444444444444n;
26
+ const RHO2 = 5555555555555555555555555555555555555555555555555555555555555555n;
27
+ const PAYLOAD2 = 6666666666666666666666666666666666666666666666666666666666666666n;
28
+
29
+ function unpack(packed) {
30
+ const p = BigInt(packed);
31
+ return { mode: p >> 60n, actionIndex: p % (1n << 60n) };
32
+ }
33
+
34
+ test("zero chain matches the contract rule", () => {
35
+ assert.equal(ZEROS[0], 0n);
36
+ for (let i = 0; i < D; i++) {
37
+ assert.equal(ZEROS[i + 1], poseidon([ZEROS[i], ZEROS[i]]));
38
+ }
39
+ // Empty note tree root is zeros[D].
40
+ assert.equal(new NoteTree().root(), ZEROS[D]);
41
+ });
42
+
43
+ test("NoteTree: insert changes root; pre-insert zero proof and post-insert leaf proof verify", () => {
44
+ const tree = new NoteTree();
45
+ const note = poseidon([OWNER_KEY, RHO, PAYLOAD]);
46
+ const preRoot = tree.root();
47
+
48
+ const ins = tree.insert(note);
49
+ assert.equal(ins.index, 0);
50
+ assert.equal(ins.pathElements.length, D);
51
+ assert.equal(ins.pathIndices.length, D);
52
+
53
+ // Returned proof is the PRE-insert zero-leaf proof: zeros[0]=0 at index 0 vs preRoot.
54
+ assert.ok(verifyMerkleProof(ZEROS[0], ins.pathElements, ins.pathIndices, preRoot));
55
+ // LSB-first bits of index 0 are all zero.
56
+ assert.ok(ins.pathIndices.every((b) => b === 0));
57
+
58
+ const postRoot = tree.root();
59
+ assert.notEqual(postRoot, preRoot);
60
+ // Post-insert membership proof of the actual leaf verifies vs postRoot.
61
+ const mem = tree.getProof(0);
62
+ assert.ok(verifyMerkleProof(note, mem.pathElements, mem.pathIndices, postRoot));
63
+ // ...and NOT vs the stale root (replay protection shape).
64
+ assert.ok(!verifyMerkleProof(note, mem.pathElements, mem.pathIndices, preRoot));
65
+
66
+ // Second insert at index 1: sibling at level 0 is the first leaf.
67
+ const note2 = poseidon([OWNER_KEY2, RHO2, PAYLOAD2]);
68
+ const pre2 = tree.root();
69
+ const ins2 = tree.insert(note2);
70
+ assert.equal(ins2.index, 1);
71
+ assert.equal(ins2.pathElements[0], note); // level-0 sibling
72
+ assert.equal(ins2.pathIndices[0], 1); // LSB of index 1
73
+ assert.ok(verifyMerkleProof(ZEROS[0], ins2.pathElements, ins2.pathIndices, pre2));
74
+ assert.notEqual(tree.root(), pre2);
75
+ });
76
+
77
+ test("NoteTree: inserting a zero leaf at a zero position is a root no-op", () => {
78
+ const tree = new NoteTree();
79
+ const r0 = tree.root();
80
+ tree.insert(0n);
81
+ assert.equal(tree.root(), r0);
82
+ // Still a no-op above a non-zero leaf: zero subtree paths stay the zero chain.
83
+ const tree2 = new NoteTree();
84
+ tree2.insert(poseidon([1n, 2n, 3n]));
85
+ const before = tree2.root();
86
+ tree2.insert(0n);
87
+ tree2.insert(0n);
88
+ assert.equal(tree2.root(), before);
89
+ });
90
+
91
+ test("NullifierIMT: genesis shape and root", () => {
92
+ const imt = new NullifierIMT();
93
+ assert.equal(imt.appendCounter, 1); // genesis leaf occupies index 0
94
+ assert.deepEqual(imt.leaves[0], { value: 0n, nextIndex: 0n, nextValue: FIELD_R - 1n });
95
+ const genesisHash = poseidon([0n, 0n, FIELD_R - 1n]);
96
+ assert.equal(imt.leafHashes()[0], genesisHash);
97
+ // Genesis root = Merkle root of the single genesis leaf.
98
+ const proof = imt.getProof(0);
99
+ assert.ok(imt.verifyProof(imt.leaves[0], proof.pathElements, proof.pathIndices, imt.root()));
100
+ });
101
+
102
+ test("NullifierIMT: insert updates predecessor 0, appends new leaf, changes root", () => {
103
+ const imt = new NullifierIMT();
104
+ const nf = poseidon([DOMAIN_REVOCATION, OWNER_KEY, RHO]);
105
+ const oldRoot = imt.root();
106
+
107
+ const res = imt.insert(nf);
108
+ assert.equal(res.newLeafIndex, 1);
109
+ assert.deepEqual(res.predecessorLeaf, { value: 0n, nextIndex: 0n, nextValue: FIELD_R - 1n });
110
+
111
+ // Predecessor path proves the OLD genesis leaf vs the OLD root.
112
+ assert.ok(
113
+ imt.verifyProof(
114
+ res.predecessorLeaf,
115
+ res.predecessorPath.pathElements,
116
+ res.predecessorPath.pathIndices,
117
+ oldRoot,
118
+ ),
119
+ );
120
+
121
+ // Intermediate state = predecessor updated, nothing appended yet.
122
+ const intermediate = new NullifierIMT();
123
+ intermediate.leaves[0] = { value: 0n, nextIndex: 1n, nextValue: nf };
124
+ const intermediateRoot = intermediate.root();
125
+ // appendPath is the zero-leaf proof at newLeafIndex vs the intermediate root.
126
+ assert.ok(
127
+ verifyMerkleProof(ZEROS[0], res.appendPath.pathElements, res.appendPath.pathIndices, intermediateRoot),
128
+ );
129
+
130
+ // Final state: predecessor points at the new leaf; new leaf inherits old pointer.
131
+ assert.deepEqual(imt.leaves[0], { value: 0n, nextIndex: 1n, nextValue: nf });
132
+ assert.deepEqual(imt.leaves[1], { value: nf, nextIndex: 0n, nextValue: FIELD_R - 1n });
133
+ intermediate.leaves.push({ value: nf, nextIndex: 0n, nextValue: FIELD_R - 1n });
134
+ assert.equal(imt.root(), intermediate.root());
135
+ assert.notEqual(imt.root(), oldRoot);
136
+ assert.notEqual(imt.root(), intermediateRoot);
137
+
138
+ // New leaf membership verifies vs the new root.
139
+ const mem = imt.getProof(1);
140
+ assert.ok(imt.verifyProof(imt.leaves[1], mem.pathElements, mem.pathIndices, imt.root()));
141
+ });
142
+
143
+ test("NullifierIMT: second insert picks the correct predecessor (above and below)", () => {
144
+ const imt = new NullifierIMT();
145
+ const nf1 = poseidon([DOMAIN_REVOCATION, OWNER_KEY, RHO]);
146
+ const nf2 = poseidon([DOMAIN_REVOCATION, OWNER_KEY2, RHO2]);
147
+ assert.notEqual(nf1, nf2);
148
+ const [lo, hi] = nf1 < nf2 ? [nf1, nf2] : [nf2, nf1];
149
+
150
+ imt.insert(hi); // index 1
151
+ // Insert above hi: predecessor must be the hi leaf (index 1), not genesis.
152
+ const above = hi + 1n < FIELD_R ? hi + 1n : hi - 1n;
153
+ const resAbove = imt.insert(above);
154
+ assert.equal(resAbove.predecessorLeaf.value, hi);
155
+ assert.equal(resAbove.newLeafIndex, 2);
156
+ // Insert between genesis and lo... use lo itself inserted below hi first.
157
+ const resLo = imt.insert(lo);
158
+ assert.equal(resLo.predecessorLeaf.value, 0n); // genesis is the largest value < lo
159
+ assert.equal(resLo.newLeafIndex, 3);
160
+ // And a mid value between lo and hi picks lo.
161
+ const mid = lo + (hi - lo) / 2n;
162
+ const resMid = imt.insert(mid);
163
+ assert.equal(resMid.predecessorLeaf.value, lo);
164
+ assert.equal(resMid.newLeafIndex, 4);
165
+ });
166
+
167
+ test("NullifierIMT: duplicate nullifier rejected by strict inequality", () => {
168
+ const imt = new NullifierIMT();
169
+ const nf = poseidon([DOMAIN_REVOCATION, OWNER_KEY, RHO]);
170
+ imt.insert(nf);
171
+ assert.throws(() => imt.insert(nf), /strict inequality|duplicate/);
172
+ // State untouched after rejection.
173
+ assert.equal(imt.appendCounter, 2);
174
+ assert.equal(imt.leaves.filter((l) => l.value === nf).length, 1);
175
+ // Out-of-field nullifiers rejected too.
176
+ assert.throws(() => imt.insert(0n), RangeError);
177
+ assert.throws(() => imt.insert(FIELD_R), RangeError);
178
+ });
179
+
180
+ test("witness APPEND: roots, packed KAT, zero-leaf path, unused signals zeroed", () => {
181
+ const noteTree = new NoteTree();
182
+ const imt = new NullifierIMT();
183
+ const note = poseidon([OWNER_KEY, RHO, PAYLOAD]);
184
+ const oldNoteRoot = noteTree.root();
185
+ const oldNullRoot = imt.root();
186
+
187
+ const w = buildActionWitness({
188
+ mode: "append",
189
+ noteTree,
190
+ nullifierTree: imt,
191
+ ownerKey: OWNER_KEY,
192
+ rho: RHO,
193
+ payloadCommit: PAYLOAD,
194
+ });
195
+
196
+ // Public signals — hand-computed expectations.
197
+ assert.equal(w.oldNoteRoot, oldNoteRoot.toString());
198
+ assert.equal(w.oldNullRoot, oldNullRoot.toString());
199
+ assert.equal(w.newNullRoot, oldNullRoot.toString()); // constrained equal
200
+ assert.equal(w.nullifier, "0");
201
+ // packed KAT: mode 0 * 2^60 + actionIndex 0 = 0 for the very first append.
202
+ assert.equal(w.packed, "0");
203
+ assert.deepEqual(unpack(w.packed), { mode: 0n, actionIndex: 0n });
204
+ // newNoteRoot = independently recomputed root of [note].
205
+ assert.equal(BigInt(w.newNoteRoot), new NoteTree(20, [note]).root());
206
+ assert.equal(BigInt(w.newNoteRoot), noteTree.root()); // tree mutated in place
207
+
208
+ // Private signals.
209
+ assert.equal(w.isAppend, "1");
210
+ assert.equal(w.note, note.toString());
211
+ assert.equal(w.ownerKey, OWNER_KEY.toString());
212
+ assert.equal(w.rho, RHO.toString());
213
+ assert.equal(w.payloadCommit, PAYLOAD.toString());
214
+ // notePath* = pre-insert zero-leaf proof vs oldNoteRoot.
215
+ assert.equal(w.notePathElements.length, D);
216
+ assert.ok(
217
+ verifyMerkleProof(
218
+ ZEROS[0],
219
+ w.notePathElements.map(BigInt),
220
+ w.notePathIndices.map(Number),
221
+ oldNoteRoot,
222
+ ),
223
+ );
224
+ // Unused revocation signals are zero paths / zero scalars (fixed shape).
225
+ for (const k of ["noteMemPathElements", "predPathElements", "appendPathElements"]) {
226
+ assert.ok(w[k].every((x) => x === "0"), `${k} must be all zeros in append mode`);
227
+ }
228
+ for (const k of ["noteMemPathIndices", "predPathIndices", "appendPathIndices"]) {
229
+ assert.ok(w[k].every((x) => x === "0"), `${k} must be all zeros in append mode`);
230
+ }
231
+ assert.equal(w.predValue, "0");
232
+ assert.equal(w.predNextIndex, "0");
233
+ assert.equal(w.predNextValue, "0");
234
+
235
+ // Second append: packed = actionIndex 1 (still mode 0).
236
+ const w2 = buildActionWitness({
237
+ mode: "append",
238
+ noteTree,
239
+ nullifierTree: imt,
240
+ ownerKey: OWNER_KEY2,
241
+ rho: RHO2,
242
+ payloadCommit: PAYLOAD2,
243
+ });
244
+ assert.deepEqual(unpack(w2.packed), { mode: 0n, actionIndex: 1n });
245
+ assert.equal(BigInt(w2.oldNoteRoot), BigInt(w.newNoteRoot)); // continuity
246
+ });
247
+
248
+ test("witness REVOKE: membership, nullifier, IMT transition, packed KAT", () => {
249
+ const noteTree = new NoteTree();
250
+ const imt = new NullifierIMT();
251
+ // Two prior appends so membership happens above the bottom row.
252
+ buildActionWitness({
253
+ mode: "append",
254
+ noteTree,
255
+ nullifierTree: imt,
256
+ ownerKey: OWNER_KEY2,
257
+ rho: RHO2,
258
+ payloadCommit: PAYLOAD2,
259
+ });
260
+ buildActionWitness({
261
+ mode: "append",
262
+ noteTree,
263
+ nullifierTree: imt,
264
+ ownerKey: OWNER_KEY,
265
+ rho: RHO,
266
+ payloadCommit: PAYLOAD,
267
+ });
268
+ const note = poseidon([OWNER_KEY, RHO, PAYLOAD]);
269
+ const nf = poseidon([DOMAIN_REVOCATION, OWNER_KEY, RHO]);
270
+ const oldNoteRoot = noteTree.root();
271
+ const oldNullRoot = imt.root();
272
+
273
+ const w = buildActionWitness({
274
+ mode: "revoke",
275
+ noteTree,
276
+ nullifierTree: imt,
277
+ ownerKey: OWNER_KEY,
278
+ rho: RHO,
279
+ payloadCommit: PAYLOAD,
280
+ noteIndex: 1,
281
+ });
282
+
283
+ // Public signals — hand-computed expectations.
284
+ assert.equal(w.oldNoteRoot, oldNoteRoot.toString());
285
+ assert.equal(w.newNoteRoot, oldNoteRoot.toString()); // constrained equal
286
+ assert.equal(w.oldNullRoot, oldNullRoot.toString());
287
+ assert.equal(w.nullifier, nf.toString());
288
+ // packed KAT: first revoke → mode 1 * 2^60 + actionIndex 1 (genesis counts).
289
+ assert.equal(BigInt(w.packed), (1n << 60n) + 1n);
290
+ assert.deepEqual(unpack(w.packed), { mode: 1n, actionIndex: 1n });
291
+ assert.notEqual(w.newNullRoot, oldNullRoot.toString());
292
+ assert.equal(BigInt(w.newNullRoot), imt.root()); // IMT mutated in place
293
+
294
+ // Private signals.
295
+ assert.equal(w.isAppend, "0");
296
+ assert.equal(w.note, note.toString());
297
+ // Membership path proves the note vs oldNoteRoot.
298
+ assert.ok(
299
+ verifyMerkleProof(
300
+ note,
301
+ w.noteMemPathElements.map(BigInt),
302
+ w.noteMemPathIndices.map(Number),
303
+ oldNoteRoot,
304
+ ),
305
+ );
306
+ // LSB-first bits reconstruct noteIndex = 1.
307
+ const bits = w.noteMemPathIndices.map(Number);
308
+ assert.equal(bits.reduce((acc, b, l) => acc + BigInt(b) * (1n << BigInt(l)), 0n), 1n);
309
+ // Predecessor signals are the OLD genesis leaf.
310
+ assert.equal(w.predValue, "0");
311
+ assert.equal(w.predNextIndex, "0");
312
+ assert.equal(w.predNextValue, (FIELD_R - 1n).toString());
313
+ const genesisHash = poseidon([0n, 0n, FIELD_R - 1n]);
314
+ assert.ok(
315
+ verifyMerkleProof(
316
+ genesisHash,
317
+ w.predPathElements.map(BigInt),
318
+ w.predPathIndices.map(Number),
319
+ oldNullRoot,
320
+ ),
321
+ );
322
+ // appendPath: zero leaf at index 1 vs the intermediate root.
323
+ const intermediate = new NullifierIMT();
324
+ intermediate.leaves[0] = { value: 0n, nextIndex: 1n, nextValue: nf };
325
+ assert.ok(
326
+ verifyMerkleProof(
327
+ ZEROS[0],
328
+ w.appendPathElements.map(BigInt),
329
+ w.appendPathIndices.map(Number),
330
+ intermediate.root(),
331
+ ),
332
+ );
333
+ // Unused note-insert path is all zeros in revoke mode.
334
+ assert.ok(w.notePathElements.every((x) => x === "0"));
335
+ assert.ok(w.notePathIndices.every((x) => x === "0"));
336
+
337
+ // Double revoke of the same note → duplicate nullifier rejected.
338
+ assert.throws(
339
+ () =>
340
+ buildActionWitness({
341
+ mode: "revoke",
342
+ noteTree,
343
+ nullifierTree: imt,
344
+ ownerKey: OWNER_KEY,
345
+ rho: RHO,
346
+ payloadCommit: PAYLOAD,
347
+ noteIndex: 1,
348
+ }),
349
+ /strict inequality|duplicate/,
350
+ );
351
+
352
+ // Second distinct revoke continues the sequential append counter.
353
+ const w2 = buildActionWitness({
354
+ mode: "revoke",
355
+ noteTree,
356
+ nullifierTree: imt,
357
+ ownerKey: OWNER_KEY2,
358
+ rho: RHO2,
359
+ payloadCommit: PAYLOAD2,
360
+ noteIndex: 0,
361
+ });
362
+ assert.deepEqual(unpack(w2.packed), { mode: 1n, actionIndex: 2n });
363
+ assert.equal(BigInt(w2.oldNullRoot), BigInt(w.newNullRoot)); // continuity
364
+ });
365
+
366
+ test("witness input validation", () => {
367
+ const noteTree = new NoteTree();
368
+ const imt = new NullifierIMT();
369
+ assert.throws(
370
+ () => buildActionWitness({ mode: "append", noteTree, nullifierTree: imt, ownerKey: 1n, rho: 2n }),
371
+ /missing payloadCommit/,
372
+ );
373
+ assert.throws(
374
+ () =>
375
+ buildActionWitness({
376
+ mode: "explode",
377
+ noteTree,
378
+ nullifierTree: imt,
379
+ ownerKey: 1n,
380
+ rho: 2n,
381
+ payloadCommit: 3n,
382
+ }),
383
+ /unknown mode/,
384
+ );
385
+ // Mismatched explicit note rejected (circuit constrains note = Poseidon(opening)).
386
+ assert.throws(
387
+ () =>
388
+ buildActionWitness({
389
+ mode: "append",
390
+ noteTree,
391
+ nullifierTree: imt,
392
+ ownerKey: 1n,
393
+ rho: 2n,
394
+ payloadCommit: 3n,
395
+ note: 999n,
396
+ }),
397
+ /does not match/,
398
+ );
399
+ // noteIndex must point at a leaf matching the opening.
400
+ assert.throws(
401
+ () =>
402
+ buildActionWitness({
403
+ mode: "revoke",
404
+ noteTree,
405
+ nullifierTree: imt,
406
+ ownerKey: 1n,
407
+ rho: 2n,
408
+ payloadCommit: 3n,
409
+ noteIndex: 0,
410
+ }),
411
+ RangeError,
412
+ );
413
+ });
414
+
415
+ test("DOMAIN_REVOCATION is the contract constant", () => {
416
+ assert.equal(DOMAIN_REVOCATION, BigInt("0x656e69676d615f7265766f6b65"));
417
+ assert.equal(DOMAIN_REVOCATION.toString(), "8036215190653564882703377984357");
418
+ assert.ok(DOMAIN_REVOCATION < FIELD_R);
419
+ });