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.
- package/README.md +76 -24
- package/apps/cli/bin/enigma-desktop.mjs +140 -0
- package/apps/cli/bin/enigma-terminal.mjs +78 -0
- package/apps/cli/bin/enigma.mjs +1923 -285
- package/apps/desktop/electron-main.cjs +217 -0
- package/apps/desktop/package.json +12 -0
- package/apps/desktop/src/app.js +264 -7
- package/apps/desktop/src/index.html +3514 -1373
- package/apps/desktop/src/launch-electron.mjs +51 -0
- package/apps/desktop/src/server.mjs +2914 -0
- package/apps/desktop/src/styles.css +2972 -260
- package/apps/desktop/src/zk-browser-prove.mjs +53 -0
- package/apps/desktop/src/zk-state.mjs +1789 -0
- package/apps/gateway/bin/enigma-gateway.mjs +102 -5
- package/apps/gateway/src/server.mjs +271 -8
- package/apps/ios/EnigmaCore/Package.swift +12 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/EnigmaAPIClient.swift +227 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/Models.swift +278 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PKCE.swift +96 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/PrivacyMinimizer.swift +187 -0
- package/apps/ios/EnigmaCore/Sources/EnigmaCore/ToolModels.swift +129 -0
- package/apps/ios/EnigmaCore/Tests/EnigmaCoreTests/EnigmaCoreTests.swift +42 -0
- package/apps/ios/EnigmaIOS/Enigma/AppModel.swift +346 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AccentColor.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/Contents.json +11 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/AppIcon.appiconset/EnigmaAppIcon.png +0 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/Contents.json +3 -0
- package/apps/ios/EnigmaIOS/Enigma/Assets.xcassets/LaunchBackground.colorset/Contents.json +12 -0
- package/apps/ios/EnigmaIOS/Enigma/ChatView.swift +181 -0
- package/apps/ios/EnigmaIOS/Enigma/CouncilView.swift +78 -0
- package/apps/ios/EnigmaIOS/Enigma/CreateView.swift +152 -0
- package/apps/ios/EnigmaIOS/Enigma/EnigmaApp.swift +52 -0
- package/apps/ios/EnigmaIOS/Enigma/Info.plist +52 -0
- package/apps/ios/EnigmaIOS/Enigma/NaturalLanguagePrivacyTagger.swift +26 -0
- package/apps/ios/EnigmaIOS/Enigma/OAuthClient.swift +321 -0
- package/apps/ios/EnigmaIOS/Enigma/OnboardingView.swift +105 -0
- package/apps/ios/EnigmaIOS/Enigma/PrivateVaultView.swift +275 -0
- package/apps/ios/EnigmaIOS/Enigma/SecureStore.swift +76 -0
- package/apps/ios/EnigmaIOS/Enigma/SettingsView.swift +60 -0
- package/apps/ios/EnigmaIOS/Enigma/Theme.swift +80 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/project.pbxproj +211 -0
- package/apps/ios/EnigmaIOS/EnigmaIOS.xcodeproj/xcshareddata/xcschemes/Enigma.xcscheme +23 -0
- package/apps/native-host/README.md +19 -8
- package/apps/native-host/bin/enigma-native-host.mjs +229 -13
- package/apps/relay/bin/enigma-relay.mjs +103 -5
- package/apps/relay/src/federation-runtime.mjs +618 -0
- package/apps/relay/src/server.mjs +310 -9
- package/apps/verifier/bin/enigma-verify.mjs +327 -11
- package/cortex-v3/circuits/build/intent_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/sale_vk_bytes.json +35 -0
- package/cortex-v3/circuits/build/vk_bytes.json +32 -0
- package/cortex-v3/proving-assets.json +64 -0
- package/cortex-v3/zk/BUILD-CONTRACT.md +87 -0
- package/cortex-v3/zk/action-transition-vk.json +119 -0
- package/cortex-v3/zk/alias-adversarial.test.mjs +220 -0
- package/cortex-v3/zk/groth16-verify-child.mjs +17 -0
- package/cortex-v3/zk/intent-witness.mjs +365 -0
- package/cortex-v3/zk/intent-witness.test.mjs +485 -0
- package/cortex-v3/zk/proving-assets.mjs +203 -0
- package/cortex-v3/zk/sale-witness.mjs +783 -0
- package/cortex-v3/zk/sale-witness.test.mjs +784 -0
- package/cortex-v3/zk/sealed-sale-release-vk.json +119 -0
- package/cortex-v3/zk/settlement-evidence.mjs +722 -0
- package/cortex-v3/zk/setup-intent.mjs +688 -0
- package/cortex-v3/zk/setup-sale.mjs +666 -0
- package/cortex-v3/zk/setup.mjs +594 -0
- package/cortex-v3/zk/witness.mjs +184 -0
- package/cortex-v3/zk/zk-codec.mjs +232 -0
- package/cortex-v3/zk/zk-codec.test.mjs +293 -0
- package/cortex-v3/zk/zk-settle.mjs +370 -0
- package/cortex-v3/zk/zk-tree.mjs +256 -0
- package/cortex-v3/zk/zk-tree.test.mjs +419 -0
- package/deploy/docker-compose.local-production-simulation.yml +36 -0
- package/docs/browser-extension-install.md +8 -6
- package/docs/client-connectors.md +15 -11
- package/docs/developer-ecosystem.md +15 -13
- package/docs/enigma-memory-ready-conformance.md +11 -9
- package/docs/install-anywhere.md +61 -28
- package/docs/installers-and-desktop.md +8 -7
- package/docs/novelty-invention-candidates.md +161 -161
- package/docs/proof-network-claim-boundaries.md +320 -318
- package/examples/01-quickstart-agent/index.mjs +49 -0
- package/examples/01_agent_memory_quickstart.mjs +57 -0
- package/examples/02-multi-agent-swarm/index.mjs +57 -0
- package/examples/02_cross_model_passport.mjs +64 -0
- package/examples/03-langchain-memory/index.mjs +41 -0
- package/examples/03_poseidon_commitment_verification.mjs +71 -0
- package/examples/04-python-trading-agent/trader.py +49 -0
- package/examples/README.md +27 -0
- package/examples/ci/github-actions.yml +7 -2
- package/package.json +142 -11
- package/packages/adapters/PACKAGE_CONTRACT.md +1 -1
- package/packages/connectors/src/index.js +196 -4
- package/packages/connectors/swarm-router.mjs +168 -0
- package/packages/core/src/index.js +248 -1
- package/packages/core/src/version.mjs +7 -0
- package/packages/dev-tools/package.json +19 -0
- package/packages/dev-tools/src/index.js +4 -0
- package/packages/dev-tools/src/memory-benchmark-suite.js +112 -0
- package/packages/dev-tools/src/swarm-simulator.js +101 -0
- package/packages/dev-tools/src/vault-inspector.js +114 -0
- package/packages/dev-tools/src/vector-benchmark.js +100 -0
- package/packages/developer-platform/src/access-credentials.js +341 -0
- package/packages/developer-platform/src/http.js +132 -0
- package/packages/developer-platform/src/index.js +4 -0
- package/packages/developer-platform/src/usage-http.js +60 -0
- package/packages/developer-platform/src/usage.js +295 -0
- package/packages/enclave-runtime/attestation.mjs +159 -0
- package/packages/enclave-runtime/index.mjs +47 -0
- package/packages/enclave-runtime/session-manager.mjs +253 -0
- package/packages/enclave-runtime/zeroization-proof.mjs +227 -0
- package/packages/enigma-reflex/package.json +14 -0
- package/packages/enigma-reflex/src/index.js +204 -0
- package/packages/enigma-reflex/training/generate-dataset.mjs +40 -0
- package/packages/enigma-reflex/training/requirements.txt +8 -0
- package/packages/enigma-reflex/training/train.py +314 -0
- package/packages/enigma-weave/LICENSE +22 -0
- package/packages/enigma-weave/UPSTREAM.json +21 -0
- package/packages/enigma-weave/package.json +14 -0
- package/packages/enigma-weave/src/index.js +286 -0
- package/packages/hosted-cloud/src/index.js +80 -5
- package/packages/importers/src/index.js +432 -0
- package/packages/inference-runtime/src/browser.js +401 -0
- package/packages/inference-runtime/src/chat.js +265 -0
- package/packages/inference-runtime/src/code.js +407 -0
- package/packages/inference-runtime/src/contracts.js +162 -0
- package/packages/inference-runtime/src/http.js +232 -0
- package/packages/inference-runtime/src/image.js +186 -0
- package/packages/inference-runtime/src/index.js +10 -0
- package/packages/inference-runtime/src/model-router.js +320 -0
- package/packages/inference-runtime/src/platform.js +125 -0
- package/packages/inference-runtime/src/privacy.js +400 -0
- package/packages/inference-runtime/src/video.js +253 -0
- package/packages/mcp-server/README.md +22 -6
- package/packages/mcp-server/bin/enigma-mcp.mjs +2 -1
- package/packages/mcp-server/src/index.js +1418 -105
- package/packages/mcp-server/src/oauth.js +561 -0
- package/packages/mcp-server/src/private-handoff.js +84 -0
- package/packages/mcp-server/src/remote-http.js +273 -0
- package/packages/mcp-server/src/remote-policy.js +72 -0
- package/packages/mcp-server/swarm-bridge.mjs +361 -0
- package/packages/mesh/index.d.ts +283 -0
- package/packages/mesh/package.json +23 -0
- package/packages/mesh/src/crypto.js +189 -0
- package/packages/mesh/src/federation-packets.js +353 -0
- package/packages/mesh/src/gossip.js +311 -0
- package/packages/mesh/src/index.js +6 -0
- package/packages/mesh/src/protocol.js +255 -0
- package/packages/mesh/src/router.js +279 -0
- package/packages/mesh/src/transport.js +306 -0
- package/packages/passport/src/index.js +426 -1
- package/packages/private-economy/src/credits-http.js +100 -0
- package/packages/private-economy/src/credits.js +447 -0
- package/packages/private-economy/src/index.js +5 -0
- package/packages/private-economy/src/payments-http.js +120 -0
- package/packages/private-economy/src/payments.js +509 -0
- package/packages/private-economy/src/x402.js +346 -0
- package/packages/proof-network/PACKAGE_CONTRACT.md +21 -0
- package/packages/rag/index.d.ts +182 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/THIRD_PARTY_LICENSES.txt +207 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/config.json +25 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/onnx/model_quantized.onnx +0 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/sha256-manifest.json +28 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer.json +30686 -0
- package/packages/rag/models/Xenova/all-MiniLM-L6-v2/tokenizer_config.json +15 -0
- package/packages/rag/package.json +27 -0
- package/packages/rag/src/blinded-search.js +109 -0
- package/packages/rag/src/bm25.js +169 -0
- package/packages/rag/src/embeddings.js +459 -0
- package/packages/rag/src/hybrid.js +76 -0
- package/packages/rag/src/index.js +38 -0
- package/packages/rag/src/reranker.js +61 -0
- package/packages/rag/src/research.js +107 -0
- package/packages/rag/src/vector-store.js +430 -0
- package/packages/rag/src/verify-model-artifacts.mjs +4 -0
- package/packages/sdk/index.d.ts +760 -0
- package/packages/sdk/package.json +33 -0
- package/packages/sdk/python/README.md +24 -0
- package/packages/sdk/python/enigma_sdk.py +250 -0
- package/packages/sdk/python/pyproject.toml +34 -0
- package/packages/sdk/python/requirements.txt +1 -0
- package/packages/sdk/python/setup.py +20 -0
- package/packages/sdk/src/federation/capability-grant.js +389 -0
- package/packages/sdk/src/federation/federation-router.js +360 -0
- package/packages/sdk/src/federation/ghostmesh-bridge.js +497 -0
- package/packages/sdk/src/federation/index.js +3 -0
- package/packages/sdk/src/index.js +1796 -0
- package/packages/sdk/src/intelligence/contradiction.js +337 -0
- package/packages/sdk/src/intelligence/decision-engine.js +155 -0
- package/packages/sdk/src/intelligence/index.js +4 -0
- package/packages/sdk/src/intelligence/ontology.js +122 -0
- package/packages/sdk/src/intelligence/temporal.js +123 -0
- package/packages/sdk/src/market-client.js +142 -0
- package/packages/sdk/src/mesh-client.js +110 -0
- package/packages/sdk/src/middleware/index.js +3 -0
- package/packages/sdk/src/middleware/langchain.js +159 -0
- package/packages/sdk/src/middleware/llamaindex.js +101 -0
- package/packages/sdk/src/middleware/vercel-ai.js +112 -0
- package/packages/sdk/src/rag-client.js +85 -0
- package/packages/sdk/src/swarm-orchestrator.js +260 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +1 -1
- package/packages/snapcompact/THIRD_PARTY_LICENSES.txt +40 -0
- package/packages/snapcompact/assets/8x13-latin1.bdf +3837 -0
- package/packages/snapcompact/index.d.ts +284 -0
- package/packages/snapcompact/package.json +25 -0
- package/packages/snapcompact/src/index.js +716 -0
- package/packages/storage/PACKAGE_CONTRACT.md +1 -1
- package/packages/terminal-console/animations.mjs +240 -0
- package/packages/terminal-console/auto-anchor.mjs +220 -0
- package/packages/terminal-console/banner.mjs +91 -0
- package/packages/terminal-console/commands.mjs +459 -0
- package/packages/terminal-console/delegation.mjs +152 -0
- package/packages/terminal-console/index.mjs +5 -0
- package/packages/terminal-console/outbox.mjs +143 -0
- package/packages/terminal-console/phantom-bridge.mjs +637 -0
- package/packages/terminal-console/repl.mjs +136 -0
- package/packages/terminal-console/signer-store.mjs +130 -0
- package/packages/terminal-console/solana-rpc.mjs +214 -0
- package/packages/terminal-console/solana-transport.mjs +189 -0
- package/packages/terminal-tui/dashboard.mjs +214 -0
- package/packages/terminal-tui/index.mjs +28 -0
- package/packages/terminal-tui/merkle-tree-renderer.mjs +268 -0
- package/packages/terminal-tui/telemetry-hud.mjs +137 -0
- package/packages/vault/index.d.ts +449 -0
- package/packages/vault/package.json +27 -0
- package/packages/vault/src/e2ee.mjs +393 -0
- package/packages/vault/src/enclave.js +481 -0
- package/packages/vault/src/erasure.js +207 -0
- package/packages/vault/src/index.js +1018 -155
- package/packages/vault/src/persistence.js +307 -0
- package/packages/vault/src/poseidon.js +354 -0
- package/packages/vault/src/receipt.js +459 -0
- package/scripts/benchmark-optical-context.mjs +166 -0
- package/scripts/bootstrap-enigma.mjs +502 -0
- package/scripts/build-edge-backend-workers.mjs +20 -5
- package/scripts/build-goal-completion-audit.mjs +72 -25
- package/scripts/build-hosted-api-key-lifecycle.mjs +26 -8
- package/scripts/build-hosted-customer-lifecycle.mjs +20 -3
- package/scripts/build-hosted-probe-worker.mjs +19 -4
- package/scripts/build-installer-assets.mjs +41 -21
- package/scripts/build-operator-evidence-starter.mjs +59 -1
- package/scripts/build-production-backend-env-kit.mjs +2 -0
- package/scripts/build-production-unblocker.mjs +3 -0
- package/scripts/check.mjs +17 -3
- package/scripts/collect-hosted-backend-live-evidence.mjs +49 -12
- package/scripts/install-enigma-local.mjs +18 -5
- package/scripts/release-audit.mjs +65 -109
- package/scripts/release-provenance.mjs +6 -0
- package/scripts/run-backend-readiness-smoke.mjs +112 -10
- package/scripts/scan-secrets.mjs +1 -0
- package/scripts/simulate-production-env.mjs +7 -2
- package/scripts/validate-hosted-backend-live.mjs +112 -1
- package/specs/antibody-pack-v1.schema.json +95 -0
- package/specs/antigen-envelope-v1.schema.json +81 -0
- package/specs/boundary-manifest-v1.schema.json +35 -35
- package/specs/capsule-v1.schema.json +55 -55
- package/specs/claim-boundary-manifest-v1.schema.json +22 -22
- package/specs/claim-ledger-v1.schema.json +291 -0
- package/specs/context-passport-v1.schema.json +59 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -26
- package/specs/evidence-packet-v1.schema.json +177 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +72 -3
- package/specs/immune-scan-report-v1.schema.json +112 -0
- package/specs/lifecycle-receipt-log-v1.schema.json +67 -0
- package/specs/memory-atom-v1.schema.json +59 -0
- package/specs/memory-event-v1.schema.json +42 -42
- package/specs/passport-v1.schema.json +50 -50
- package/specs/proof-of-non-use-v1.schema.json +65 -0
- package/specs/quarantine-record-v1.schema.json +126 -0
- package/specs/receipt-v1.schema.json +61 -61
- package/specs/state-checkpoint-v1.schema.json +37 -37
- package/specs/trust-bundle-v1.schema.json +56 -56
- package/specs/trust-card-v1.schema.json +119 -0
- package/docs/proof-network-launch-plan.md +0 -421
- package/packages/metering/PACKAGE_CONTRACT.md +0 -20
- package/scripts/build-ai-orchestration-plan.mjs +0 -248
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createPrivateKey,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
sign,
|
|
6
|
+
verify,
|
|
7
|
+
} from 'node:crypto';
|
|
8
|
+
import { fork } from 'node:child_process';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const GROTH16_CHILD_PATH = fileURLToPath(new URL('./groth16-verify-child.mjs', import.meta.url));
|
|
13
|
+
|
|
14
|
+
export const ZK_SETTLEMENT_EVIDENCE_SCHEMA = 'enigma.zk_settlement_evidence.v1';
|
|
15
|
+
export const ZK_SETTLEMENT_EVIDENCE_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
export const ZK_CIRCUIT_CONSTRAINTS = Object.freeze({
|
|
18
|
+
action_transition: 90_897,
|
|
19
|
+
sealed_sale_release: 87_422,
|
|
20
|
+
});
|
|
21
|
+
export const ZK_R1CS_PINS = Object.freeze({
|
|
22
|
+
action_transition: '558b99519e36fe94a0c7ffaed2cd5bb7865fc7387de42895e70dabba434c9656',
|
|
23
|
+
sealed_sale_release: '46e3d6846000005db1a11559604dcf5efbcefa263eed78b0aef7702de91b6830',
|
|
24
|
+
});
|
|
25
|
+
export const ZK_VERIFICATION_KEY_PINS = Object.freeze({
|
|
26
|
+
action_transition: Object.freeze({
|
|
27
|
+
path: fileURLToPath(new URL('./action-transition-vk.json', import.meta.url)),
|
|
28
|
+
sha256: 'eb5101b47236848b8e47b52a6f737888dbed86314ecc01767ef2faa7c4be6fff',
|
|
29
|
+
}),
|
|
30
|
+
sealed_sale_release: Object.freeze({
|
|
31
|
+
path: fileURLToPath(new URL('./sealed-sale-release-vk.json', import.meta.url)),
|
|
32
|
+
sha256: '3a4ec2fac884177960b3acec73941d23c16ca0d7f917edf598fbff1146f2c711',
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const EVENT_CIRCUITS = Object.freeze({
|
|
37
|
+
'action.append': 'action_transition',
|
|
38
|
+
'action.revoke': 'action_transition',
|
|
39
|
+
'sale.list': 'sealed_sale_release',
|
|
40
|
+
'sale.claim': null,
|
|
41
|
+
'sale.open': null,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const TX_EVENTS = new Set(['action.append', 'action.revoke', 'sale.list', 'sale.claim']);
|
|
45
|
+
const PROOF_EVENTS = new Set(['action.append', 'action.revoke', 'sale.list']);
|
|
46
|
+
const HEX_32_RE = /^[a-f0-9]{64}$/;
|
|
47
|
+
const RECEIPT_REF_RE = /^zksettle_[a-f0-9]{64}$/;
|
|
48
|
+
const PRIVATE_KEY_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
49
|
+
const PUBLIC_KEY_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
50
|
+
const FORBIDDEN_KEY_RE = /(?:^|_)(?:witness|owner_key|ownerkey|rho|dek|buyer_secret|recipient_secret|secret_key|private_key|plaintext|content|prompt)(?:$|_)/iu;
|
|
51
|
+
|
|
52
|
+
function canonicalize(value) {
|
|
53
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
54
|
+
if (typeof value === 'number') {
|
|
55
|
+
if (!Number.isFinite(value)) throw new TypeError('evidence numbers must be finite');
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
59
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
60
|
+
throw new TypeError('evidence values must be JSON objects, arrays, or primitives');
|
|
61
|
+
}
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const key of Object.keys(value).sort()) {
|
|
64
|
+
if (value[key] === undefined) throw new TypeError(`evidence field ${key} must not be undefined`);
|
|
65
|
+
out[key] = canonicalize(value[key]);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function canonicalJson(value) {
|
|
71
|
+
return JSON.stringify(canonicalize(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sha256(value) {
|
|
75
|
+
return createHash('sha256').update(value).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assertPublicSafe(value, path = 'evidence') {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
value.forEach((entry, index) => assertPublicSafe(entry, `${path}[${index}]`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!value || typeof value !== 'object') return;
|
|
84
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
85
|
+
const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
86
|
+
if (FORBIDDEN_KEY_RE.test(normalizedKey)) {
|
|
87
|
+
throw new TypeError(`${path}.${key} is private witness material and cannot appear in public settlement evidence`);
|
|
88
|
+
}
|
|
89
|
+
assertPublicSafe(entry, `${path}.${key}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hex(value, field, expectedBytes) {
|
|
94
|
+
const bytes = Buffer.isBuffer(value) || value instanceof Uint8Array
|
|
95
|
+
? Buffer.from(value)
|
|
96
|
+
: typeof value === 'string' && /^(?:0x)?[a-f0-9]+$/i.test(value)
|
|
97
|
+
? Buffer.from(value.replace(/^0x/i, ''), 'hex')
|
|
98
|
+
: null;
|
|
99
|
+
if (!bytes || bytes.length !== expectedBytes) {
|
|
100
|
+
throw new TypeError(`${field} must be exactly ${expectedBytes} bytes`);
|
|
101
|
+
}
|
|
102
|
+
return bytes.toString('hex');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeProof(proofBytes, publicInputs, circuit) {
|
|
106
|
+
if (!proofBytes) return null;
|
|
107
|
+
const proof = {
|
|
108
|
+
a: hex(proofBytes.a, 'proof.a', 64),
|
|
109
|
+
b: hex(proofBytes.b, 'proof.b', 128),
|
|
110
|
+
c: hex(proofBytes.c, 'proof.c', 64),
|
|
111
|
+
};
|
|
112
|
+
if (!Array.isArray(publicInputs)) throw new TypeError('publicInputs must be an array');
|
|
113
|
+
const publicInputsHex = publicInputs.map((value, index) => hex(value, `publicInputs[${index}]`, 32));
|
|
114
|
+
return {
|
|
115
|
+
algorithm: 'groth16-bn254',
|
|
116
|
+
encoding: 'enigma.groth16_raw_be.c1c0.v1',
|
|
117
|
+
a: proof.a,
|
|
118
|
+
b: proof.b,
|
|
119
|
+
c: proof.c,
|
|
120
|
+
digest: `sha256:${sha256(canonicalJson(proof))}`,
|
|
121
|
+
public_inputs: publicInputsHex,
|
|
122
|
+
circuit_artifact: {
|
|
123
|
+
r1cs_sha256: `sha256:${ZK_R1CS_PINS[circuit]}`,
|
|
124
|
+
},
|
|
125
|
+
verification_key: {
|
|
126
|
+
sha256: `sha256:${ZK_VERIFICATION_KEY_PINS[circuit].sha256}`,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function signingKeys(signerSecret) {
|
|
132
|
+
if (signerSecret === undefined || signerSecret === null || signerSecret === '') {
|
|
133
|
+
throw new TypeError('signerSecret is required');
|
|
134
|
+
}
|
|
135
|
+
const material = Buffer.isBuffer(signerSecret) || signerSecret instanceof Uint8Array
|
|
136
|
+
? Buffer.from(signerSecret)
|
|
137
|
+
: Buffer.from(String(signerSecret), 'utf8');
|
|
138
|
+
const seed = createHash('sha256')
|
|
139
|
+
.update('enigma.zk.settlement.signer.v1\0', 'utf8')
|
|
140
|
+
.update(material)
|
|
141
|
+
.digest();
|
|
142
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([PRIVATE_KEY_PREFIX, seed]), format: 'der', type: 'pkcs8' });
|
|
143
|
+
const publicDer = createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
144
|
+
const publicRaw = Buffer.from(publicDer).subarray(-32);
|
|
145
|
+
seed.fill(0);
|
|
146
|
+
material.fill(0);
|
|
147
|
+
return { privateKey, publicRaw };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function makeError(code, message) {
|
|
151
|
+
return { code, message };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function signerIsTrusted(signer, trustedSigners) {
|
|
155
|
+
const entries = trustedSigners instanceof Set
|
|
156
|
+
? [...trustedSigners]
|
|
157
|
+
: Array.isArray(trustedSigners)
|
|
158
|
+
? trustedSigners
|
|
159
|
+
: [];
|
|
160
|
+
return entries.some((entry) => {
|
|
161
|
+
if (typeof entry === 'string') {
|
|
162
|
+
return entry === signer?.public_key || entry === signer?.key_id || entry === signer?.signer_id;
|
|
163
|
+
}
|
|
164
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
165
|
+
const anchors = [
|
|
166
|
+
['public_key', signer?.public_key],
|
|
167
|
+
['key_id', signer?.key_id],
|
|
168
|
+
['signer_id', signer?.signer_id],
|
|
169
|
+
].filter(([key]) => typeof entry[key] === 'string' && entry[key]);
|
|
170
|
+
return anchors.length > 0 && anchors.every(([key, value]) => entry[key] === value);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function finalizedCheckpointMatches(evidence, checkpoints) {
|
|
175
|
+
if (!Array.isArray(checkpoints)) return false;
|
|
176
|
+
return checkpoints.some((checkpoint) => (
|
|
177
|
+
checkpoint
|
|
178
|
+
&& checkpoint.receipt_ref === evidence?.receipt_ref
|
|
179
|
+
&& checkpoint.status === 'finalized'
|
|
180
|
+
&& checkpoint.transaction_signature === (evidence?.transaction?.signature ?? null)
|
|
181
|
+
&& checkpoint.program_id === evidence?.program_id
|
|
182
|
+
&& checkpoint.cluster === evidence?.cluster
|
|
183
|
+
&& checkpoint.state?.note_root === evidence?.state?.note_root
|
|
184
|
+
&& checkpoint.state?.null_root === evidence?.state?.null_root
|
|
185
|
+
&& checkpoint.state?.note_count === evidence?.state?.note_count
|
|
186
|
+
&& checkpoint.state?.null_count === evidence?.state?.null_count
|
|
187
|
+
));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function createZkSettlementEvidence({
|
|
191
|
+
event,
|
|
192
|
+
programId,
|
|
193
|
+
cluster,
|
|
194
|
+
transactionSignature = null,
|
|
195
|
+
explorerUrl = null,
|
|
196
|
+
proofBytes = null,
|
|
197
|
+
publicInputs = [],
|
|
198
|
+
state,
|
|
199
|
+
signerSecret,
|
|
200
|
+
signerId = null,
|
|
201
|
+
predecessorRefs = [],
|
|
202
|
+
createdAt = new Date().toISOString(),
|
|
203
|
+
}) {
|
|
204
|
+
if (!Object.hasOwn(EVENT_CIRCUITS, event)) throw new TypeError(`unsupported ZK settlement event: ${event}`);
|
|
205
|
+
if (typeof programId !== 'string' || !programId.trim()) throw new TypeError('programId is required');
|
|
206
|
+
if (typeof cluster !== 'string' || !cluster.trim()) throw new TypeError('cluster is required');
|
|
207
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) throw new TypeError('state must be a public JSON object');
|
|
208
|
+
assertPublicSafe(state, 'state');
|
|
209
|
+
if (!Array.isArray(predecessorRefs) || predecessorRefs.some((ref) => typeof ref !== 'string' || !RECEIPT_REF_RE.test(ref))) {
|
|
210
|
+
throw new TypeError('predecessorRefs must contain canonical ZK settlement receipt refs');
|
|
211
|
+
}
|
|
212
|
+
if (TX_EVENTS.has(event) && (typeof transactionSignature !== 'string' || !transactionSignature.trim())) {
|
|
213
|
+
throw new TypeError(`${event} evidence requires a transaction signature`);
|
|
214
|
+
}
|
|
215
|
+
const circuit = EVENT_CIRCUITS[event];
|
|
216
|
+
const proof = normalizeProof(proofBytes, publicInputs, circuit);
|
|
217
|
+
if (PROOF_EVENTS.has(event) && !proof) throw new TypeError(`${event} evidence requires proof bytes and public inputs`);
|
|
218
|
+
if (!PROOF_EVENTS.has(event) && proof) throw new TypeError(`${event} evidence must bind its proof through predecessor refs, not duplicate a witness-stage proof`);
|
|
219
|
+
if (proof && proof.public_inputs.length !== 6) throw new TypeError(`${event} evidence requires exactly 6 public inputs`);
|
|
220
|
+
|
|
221
|
+
const { privateKey, publicRaw } = signingKeys(signerSecret);
|
|
222
|
+
const body = {
|
|
223
|
+
schema: ZK_SETTLEMENT_EVIDENCE_SCHEMA,
|
|
224
|
+
version: ZK_SETTLEMENT_EVIDENCE_VERSION,
|
|
225
|
+
event,
|
|
226
|
+
circuit,
|
|
227
|
+
constraint_count: circuit ? ZK_CIRCUIT_CONSTRAINTS[circuit] : null,
|
|
228
|
+
program_id: programId.trim(),
|
|
229
|
+
cluster: cluster.trim(),
|
|
230
|
+
transaction: transactionSignature ? {
|
|
231
|
+
signature: transactionSignature.trim(),
|
|
232
|
+
explorer_url: typeof explorerUrl === 'string' && explorerUrl ? explorerUrl : null,
|
|
233
|
+
} : null,
|
|
234
|
+
proof,
|
|
235
|
+
state: canonicalize(state),
|
|
236
|
+
predecessor_refs: [...new Set(predecessorRefs)].sort(),
|
|
237
|
+
created_at: new Date(createdAt).toISOString(),
|
|
238
|
+
signer: {
|
|
239
|
+
algorithm: 'Ed25519',
|
|
240
|
+
public_key: publicRaw.toString('base64'),
|
|
241
|
+
key_id: `sha256:${sha256(publicRaw)}`,
|
|
242
|
+
signer_id: typeof signerId === 'string' && signerId ? signerId : null,
|
|
243
|
+
signer_domain: `${programId.trim()}:${cluster.trim()}`,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const signedBytes = Buffer.from(canonicalJson(body), 'utf8');
|
|
247
|
+
const receiptRef = `zksettle_${sha256(signedBytes)}`;
|
|
248
|
+
return {
|
|
249
|
+
...body,
|
|
250
|
+
receipt_ref: receiptRef,
|
|
251
|
+
signature: sign(null, signedBytes, privateKey).toString('base64'),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function proofToSnarkjs(proof) {
|
|
255
|
+
const limb = (value, offset) => BigInt(`0x${value.slice(offset, offset + 64)}`).toString(10);
|
|
256
|
+
return {
|
|
257
|
+
pi_a: [limb(proof.a, 0), limb(proof.a, 64), '1'],
|
|
258
|
+
pi_b: [
|
|
259
|
+
[limb(proof.b, 64), limb(proof.b, 0)],
|
|
260
|
+
[limb(proof.b, 192), limb(proof.b, 128)],
|
|
261
|
+
['1', '0'],
|
|
262
|
+
],
|
|
263
|
+
pi_c: [limb(proof.c, 0), limb(proof.c, 64), '1'],
|
|
264
|
+
protocol: 'groth16',
|
|
265
|
+
curve: 'bn128',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function loadPinnedVerificationKey(circuit, expectedHash, options) {
|
|
270
|
+
const pin = ZK_VERIFICATION_KEY_PINS[circuit];
|
|
271
|
+
if (!pin) throw new Error(`no pinned verification key for circuit ${circuit}`);
|
|
272
|
+
const path = options.verificationKeyPaths?.[circuit] ?? pin.path;
|
|
273
|
+
const raw = await readFile(path);
|
|
274
|
+
const actualHash = sha256(raw);
|
|
275
|
+
if (actualHash !== pin.sha256 || expectedHash !== `sha256:${pin.sha256}`) {
|
|
276
|
+
throw new Error(`verification key hash mismatch for ${circuit}: expected sha256:${pin.sha256}, got sha256:${actualHash}`);
|
|
277
|
+
}
|
|
278
|
+
const key = JSON.parse(raw.toString('utf8'));
|
|
279
|
+
if (key.protocol !== 'groth16' || key.curve !== 'bn128' || key.nPublic !== 6) {
|
|
280
|
+
throw new Error(`pinned verification key for ${circuit} has an invalid Groth16 contract`);
|
|
281
|
+
}
|
|
282
|
+
return key;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function verifyGroth16Binding(evidence, options) {
|
|
286
|
+
const proof = evidence.proof;
|
|
287
|
+
const verificationKey = await loadPinnedVerificationKey(
|
|
288
|
+
evidence.circuit,
|
|
289
|
+
proof.verification_key?.sha256,
|
|
290
|
+
options,
|
|
291
|
+
);
|
|
292
|
+
const publicSignals = proof.public_inputs.map((value) => BigInt(`0x${value}`).toString(10));
|
|
293
|
+
const timeoutMs = Number.isSafeInteger(options.groth16TimeoutMs) && options.groth16TimeoutMs > 0
|
|
294
|
+
? options.groth16TimeoutMs
|
|
295
|
+
: 30_000;
|
|
296
|
+
const child = fork(GROTH16_CHILD_PATH, [], {
|
|
297
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
298
|
+
});
|
|
299
|
+
return await new Promise((resolve, reject) => {
|
|
300
|
+
let settled = false;
|
|
301
|
+
const finish = (fn, value) => {
|
|
302
|
+
if (settled) return;
|
|
303
|
+
settled = true;
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
child.kill();
|
|
306
|
+
fn(value);
|
|
307
|
+
};
|
|
308
|
+
const timer = setTimeout(
|
|
309
|
+
() => finish(reject, new Error(`snarkjs verification exceeded ${timeoutMs}ms`)),
|
|
310
|
+
timeoutMs,
|
|
311
|
+
);
|
|
312
|
+
child.once('message', (message) => {
|
|
313
|
+
if (message?.error) finish(reject, new Error(message.error));
|
|
314
|
+
else finish(resolve, message?.verified === true);
|
|
315
|
+
});
|
|
316
|
+
child.once('error', (error) => finish(reject, error));
|
|
317
|
+
child.once('exit', (code) => {
|
|
318
|
+
if (!settled) finish(reject, new Error(`snarkjs verifier child exited before returning a verdict (code ${code})`));
|
|
319
|
+
});
|
|
320
|
+
child.send({
|
|
321
|
+
verificationKey,
|
|
322
|
+
publicSignals,
|
|
323
|
+
proof: proofToSnarkjs(proof),
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export { proofToSnarkjs };
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
export async function verifyZkSettlementEvidence(evidence, options = {}) {
|
|
332
|
+
let proofValid = PROOF_EVENTS.has(evidence?.event) ? false : null;
|
|
333
|
+
const errors = [];
|
|
334
|
+
if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
|
|
335
|
+
return { ok: false, verified: false, proof_valid: null, settlement_verified: false, receipt_ref: null, event: null, errors: [makeError('ZK_EVIDENCE_INVALID', 'Evidence must be a JSON object.')] };
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
assertPublicSafe(evidence, 'evidence');
|
|
339
|
+
if (evidence.schema !== ZK_SETTLEMENT_EVIDENCE_SCHEMA || evidence.version !== ZK_SETTLEMENT_EVIDENCE_VERSION) {
|
|
340
|
+
errors.push(makeError('ZK_EVIDENCE_SCHEMA_INVALID', 'Unsupported ZK settlement evidence schema or version.'));
|
|
341
|
+
}
|
|
342
|
+
if (!Object.hasOwn(EVENT_CIRCUITS, evidence.event)) {
|
|
343
|
+
errors.push(makeError('ZK_EVIDENCE_EVENT_INVALID', 'Unsupported ZK settlement event.'));
|
|
344
|
+
} else {
|
|
345
|
+
const circuit = EVENT_CIRCUITS[evidence.event];
|
|
346
|
+
if (evidence.circuit !== circuit || evidence.constraint_count !== (circuit ? ZK_CIRCUIT_CONSTRAINTS[circuit] : null)) {
|
|
347
|
+
errors.push(makeError('ZK_EVIDENCE_CIRCUIT_INVALID', 'Circuit or constraint label does not match the settlement event.'));
|
|
348
|
+
}
|
|
349
|
+
if (TX_EVENTS.has(evidence.event) && !evidence.transaction?.signature) {
|
|
350
|
+
errors.push(makeError('ZK_EVIDENCE_TX_MISSING', 'Settlement transaction signature is missing.'));
|
|
351
|
+
}
|
|
352
|
+
if (PROOF_EVENTS.has(evidence.event)) {
|
|
353
|
+
const rawProof = {
|
|
354
|
+
a: evidence.proof?.a,
|
|
355
|
+
b: evidence.proof?.b,
|
|
356
|
+
c: evidence.proof?.c,
|
|
357
|
+
};
|
|
358
|
+
const rawProofValid = /^[a-f0-9]{128}$/.test(rawProof.a ?? '')
|
|
359
|
+
&& /^[a-f0-9]{256}$/.test(rawProof.b ?? '')
|
|
360
|
+
&& /^[a-f0-9]{128}$/.test(rawProof.c ?? '');
|
|
361
|
+
const digestValid = rawProofValid
|
|
362
|
+
&& evidence.proof?.digest === `sha256:${sha256(canonicalJson(rawProof))}`;
|
|
363
|
+
const pin = ZK_VERIFICATION_KEY_PINS[circuit];
|
|
364
|
+
if (
|
|
365
|
+
evidence.proof?.algorithm !== 'groth16-bn254'
|
|
366
|
+
|| evidence.proof?.encoding !== 'enigma.groth16_raw_be.c1c0.v1'
|
|
367
|
+
|| !digestValid
|
|
368
|
+
|| evidence.proof?.verification_key?.sha256 !== `sha256:${pin?.sha256 ?? ''}`
|
|
369
|
+
|| !Array.isArray(evidence.proof?.public_inputs)
|
|
370
|
+
|| evidence.proof.public_inputs.length !== 6
|
|
371
|
+
|| evidence.proof.public_inputs.some((value) => !HEX_32_RE.test(value))
|
|
372
|
+
|| evidence.proof?.circuit_artifact?.r1cs_sha256 !== `sha256:${ZK_R1CS_PINS[circuit] ?? ''}`
|
|
373
|
+
) {
|
|
374
|
+
errors.push(makeError('ZK_EVIDENCE_PROOF_INVALID', 'Canonical Groth16 proof, verification-key pin, or public inputs are invalid.'));
|
|
375
|
+
}
|
|
376
|
+
} else if (evidence.proof !== null) {
|
|
377
|
+
errors.push(makeError('ZK_EVIDENCE_PROOF_UNEXPECTED', 'Non-proving lifecycle events must reference predecessor evidence instead of embedding a proof binding.'));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (typeof options.expectedProgramId !== 'string' || !options.expectedProgramId) {
|
|
381
|
+
errors.push(makeError('ZK_EVIDENCE_TRUST_REQUIRED', 'Verification requires an expectedProgramId trust anchor.'));
|
|
382
|
+
} else if (evidence.program_id !== options.expectedProgramId) {
|
|
383
|
+
errors.push(makeError('ZK_EVIDENCE_PROGRAM_MISMATCH', 'Evidence program_id does not match the trusted program.'));
|
|
384
|
+
}
|
|
385
|
+
if (typeof options.expectedCluster !== 'string' || !options.expectedCluster) {
|
|
386
|
+
errors.push(makeError('ZK_EVIDENCE_TRUST_REQUIRED', 'Verification requires an expectedCluster trust anchor.'));
|
|
387
|
+
} else if (evidence.cluster !== options.expectedCluster) {
|
|
388
|
+
errors.push(makeError('ZK_EVIDENCE_CLUSTER_MISMATCH', 'Evidence cluster does not match the trusted cluster.'));
|
|
389
|
+
}
|
|
390
|
+
if (!signerIsTrusted(evidence.signer, options.trustedSigners)) {
|
|
391
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_UNTRUSTED', 'Evidence signer is not present in the trusted signer registry.'));
|
|
392
|
+
}
|
|
393
|
+
if (evidence.signer?.signer_domain !== `${evidence.program_id}:${evidence.cluster}`) {
|
|
394
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_DOMAIN_INVALID', 'Evidence signer domain is not bound to program_id and cluster.'));
|
|
395
|
+
}
|
|
396
|
+
if (!finalizedCheckpointMatches(evidence, options.finalizedCheckpoints)) {
|
|
397
|
+
errors.push(makeError('ZK_EVIDENCE_FINALITY_UNTRUSTED', 'Evidence is not bound to a trusted finalized checkpoint.'));
|
|
398
|
+
}
|
|
399
|
+
if (!evidence.state || !HEX_32_RE.test(evidence.state.note_root ?? '') || !HEX_32_RE.test(evidence.state.null_root ?? '')) {
|
|
400
|
+
errors.push(makeError('ZK_EVIDENCE_STATE_INVALID', 'Evidence must bind canonical 32-byte note_root and null_root values.'));
|
|
401
|
+
}
|
|
402
|
+
const inputs = evidence.proof?.public_inputs;
|
|
403
|
+
if (Array.isArray(inputs) && inputs.length === 6) {
|
|
404
|
+
if (evidence.event === 'action.append' || evidence.event === 'action.revoke') {
|
|
405
|
+
const append = evidence.event === 'action.append';
|
|
406
|
+
const packed = HEX_32_RE.test(inputs[5] ?? '') ? BigInt(`0x${inputs[5]}`) : -1n;
|
|
407
|
+
const mode = packed >= 0n ? Number(packed >> 60n) : -1;
|
|
408
|
+
const state = evidence.state ?? {};
|
|
409
|
+
const countFieldsValid = [
|
|
410
|
+
state.note_count,
|
|
411
|
+
state.null_count,
|
|
412
|
+
state.previous_note_count,
|
|
413
|
+
state.previous_null_count,
|
|
414
|
+
].every((value) => Number.isSafeInteger(value) && value >= 0);
|
|
415
|
+
const rootsValid = state.note_root === inputs[1]
|
|
416
|
+
&& state.null_root === inputs[3]
|
|
417
|
+
&& state.previous_note_root === inputs[0]
|
|
418
|
+
&& state.previous_null_root === inputs[2];
|
|
419
|
+
const modeValid = mode === (append ? 0 : 1);
|
|
420
|
+
const nullifierValid = append
|
|
421
|
+
? inputs[4] === '0'.repeat(64)
|
|
422
|
+
: HEX_32_RE.test(inputs[4] ?? '') && inputs[4] !== '0'.repeat(64);
|
|
423
|
+
const invariantValid = append
|
|
424
|
+
? inputs[2] === inputs[3]
|
|
425
|
+
&& state.note_count === state.previous_note_count + 1
|
|
426
|
+
&& state.null_count === state.previous_null_count
|
|
427
|
+
: inputs[0] === inputs[1]
|
|
428
|
+
&& state.note_count === state.previous_note_count
|
|
429
|
+
&& state.null_count === state.previous_null_count + 1;
|
|
430
|
+
if (!rootsValid || !modeValid || !nullifierValid || !countFieldsValid || !invariantValid) {
|
|
431
|
+
errors.push(makeError('ZK_EVIDENCE_ACTION_TRANSITION_INVALID', 'Action label, packed mode, roots, or count transition is inconsistent with the proof public inputs.'));
|
|
432
|
+
}
|
|
433
|
+
} else if (evidence.event === 'sale.list') {
|
|
434
|
+
const saleBindings = [
|
|
435
|
+
['note_root', 0],
|
|
436
|
+
['payload_commit_pos', 1],
|
|
437
|
+
['cipher_commit', 2],
|
|
438
|
+
['key_commit', 3],
|
|
439
|
+
['sale_terms_hash', 4],
|
|
440
|
+
['envelope_commit', 5],
|
|
441
|
+
];
|
|
442
|
+
if (saleBindings.some(([field, index]) => evidence.state?.[field] !== inputs[index])) {
|
|
443
|
+
errors.push(makeError('ZK_EVIDENCE_STATE_BINDING_INVALID', 'Sale state commitments do not match the proof public inputs.'));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!Array.isArray(evidence.predecessor_refs) || evidence.predecessor_refs.some((ref) => !RECEIPT_REF_RE.test(ref))) {
|
|
448
|
+
errors.push(makeError('ZK_EVIDENCE_PREDECESSOR_INVALID', 'predecessor_refs contains an invalid receipt reference.'));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const { receipt_ref: receiptRef, signature, ...body } = evidence;
|
|
452
|
+
const signedBytes = Buffer.from(canonicalJson(body), 'utf8');
|
|
453
|
+
const expectedRef = `zksettle_${sha256(signedBytes)}`;
|
|
454
|
+
if (!RECEIPT_REF_RE.test(receiptRef ?? '') || receiptRef !== expectedRef) {
|
|
455
|
+
errors.push(makeError('ZK_EVIDENCE_REF_INVALID', 'receipt_ref does not match the canonical signed evidence body.'));
|
|
456
|
+
}
|
|
457
|
+
const publicRaw = Buffer.from(evidence.signer?.public_key ?? '', 'base64');
|
|
458
|
+
if (evidence.signer?.algorithm !== 'Ed25519' || publicRaw.length !== 32 || evidence.signer?.key_id !== `sha256:${sha256(publicRaw)}`) {
|
|
459
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_INVALID', 'Evidence signer metadata is invalid.'));
|
|
460
|
+
} else {
|
|
461
|
+
const publicKey = createPublicKey({ key: Buffer.concat([PUBLIC_KEY_PREFIX, publicRaw]), format: 'der', type: 'spki' });
|
|
462
|
+
let signatureBytes = null;
|
|
463
|
+
try {
|
|
464
|
+
signatureBytes = Buffer.from(signature, 'base64');
|
|
465
|
+
} catch { /* handled below */ }
|
|
466
|
+
if (!signatureBytes || signatureBytes.length !== 64 || !verify(null, signedBytes, publicKey, signatureBytes)) {
|
|
467
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNATURE_INVALID', 'Ed25519 evidence signature did not verify.'));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (
|
|
471
|
+
PROOF_EVENTS.has(evidence.event)
|
|
472
|
+
&& !errors.some((error) => error.code === 'ZK_EVIDENCE_PROOF_INVALID')
|
|
473
|
+
) {
|
|
474
|
+
try {
|
|
475
|
+
const proofVerified = await verifyGroth16Binding(evidence, options);
|
|
476
|
+
proofValid = proofVerified === true;
|
|
477
|
+
if (!proofVerified) {
|
|
478
|
+
errors.push(makeError('ZK_EVIDENCE_GROTH16_INVALID', 'snarkjs rejected the Groth16 proof against the pinned verification key.'));
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
proofValid = false;
|
|
482
|
+
errors.push(makeError('ZK_EVIDENCE_GROTH16_INVALID', `Offline Groth16 verification failed closed: ${error.message}`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (errors.length === 0 && options.seenRefs instanceof Set) {
|
|
486
|
+
if (options.seenRefs.has(receiptRef)) {
|
|
487
|
+
errors.push(makeError('ZK_EVIDENCE_REPLAY', `Receipt ${receiptRef} was already verified in this replay domain.`));
|
|
488
|
+
} else {
|
|
489
|
+
options.seenRefs.add(receiptRef);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
errors.push(makeError('ZK_EVIDENCE_INVALID', error.message));
|
|
494
|
+
}
|
|
495
|
+
const settlementVerified = errors.length === 0;
|
|
496
|
+
return {
|
|
497
|
+
ok: settlementVerified,
|
|
498
|
+
verified: settlementVerified,
|
|
499
|
+
proof_valid: proofValid,
|
|
500
|
+
settlement_verified: settlementVerified,
|
|
501
|
+
receipt_ref: typeof evidence.receipt_ref === 'string' ? evidence.receipt_ref : null,
|
|
502
|
+
event: typeof evidence.event === 'string' ? evidence.event : null,
|
|
503
|
+
errors,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function validateZkSettlementEvidence(evidence, options = {}) {
|
|
508
|
+
const errors = [];
|
|
509
|
+
if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
|
|
510
|
+
return { ok: false, preflight_valid: false, proof_valid: null, settlement_verified: false, receipt_ref: null, event: null, errors: [makeError('ZK_EVIDENCE_INVALID', 'Evidence must be a JSON object.')] };
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
assertPublicSafe(evidence, 'evidence');
|
|
514
|
+
const circuit = EVENT_CIRCUITS[evidence.event];
|
|
515
|
+
if (evidence.schema !== ZK_SETTLEMENT_EVIDENCE_SCHEMA || evidence.version !== ZK_SETTLEMENT_EVIDENCE_VERSION) {
|
|
516
|
+
errors.push(makeError('ZK_EVIDENCE_SCHEMA_INVALID', 'Unsupported ZK settlement evidence schema or version.'));
|
|
517
|
+
}
|
|
518
|
+
if (!Object.hasOwn(EVENT_CIRCUITS, evidence.event)) {
|
|
519
|
+
errors.push(makeError('ZK_EVIDENCE_EVENT_INVALID', 'Unsupported ZK settlement event.'));
|
|
520
|
+
} else if (evidence.circuit !== circuit || evidence.constraint_count !== (circuit ? ZK_CIRCUIT_CONSTRAINTS[circuit] : null)) {
|
|
521
|
+
errors.push(makeError('ZK_EVIDENCE_CIRCUIT_INVALID', 'Circuit or constraint label does not match the settlement event.'));
|
|
522
|
+
}
|
|
523
|
+
if (TX_EVENTS.has(evidence.event) && !evidence.transaction?.signature) {
|
|
524
|
+
errors.push(makeError('ZK_EVIDENCE_TX_MISSING', 'Settlement transaction signature is missing.'));
|
|
525
|
+
}
|
|
526
|
+
const inputs = evidence.proof?.public_inputs;
|
|
527
|
+
if (PROOF_EVENTS.has(evidence.event)) {
|
|
528
|
+
const rawProof = { a: evidence.proof?.a, b: evidence.proof?.b, c: evidence.proof?.c };
|
|
529
|
+
const rawProofValid = /^[a-f0-9]{128}$/.test(rawProof.a ?? '')
|
|
530
|
+
&& /^[a-f0-9]{256}$/.test(rawProof.b ?? '')
|
|
531
|
+
&& /^[a-f0-9]{128}$/.test(rawProof.c ?? '');
|
|
532
|
+
const pin = ZK_VERIFICATION_KEY_PINS[circuit];
|
|
533
|
+
if (
|
|
534
|
+
evidence.proof?.algorithm !== 'groth16-bn254'
|
|
535
|
+
|| evidence.proof?.encoding !== 'enigma.groth16_raw_be.c1c0.v1'
|
|
536
|
+
|| !rawProofValid
|
|
537
|
+
|| evidence.proof?.digest !== `sha256:${sha256(canonicalJson(rawProof))}`
|
|
538
|
+
|| evidence.proof?.verification_key?.sha256 !== `sha256:${pin?.sha256 ?? ''}`
|
|
539
|
+
|| evidence.proof?.circuit_artifact?.r1cs_sha256 !== `sha256:${ZK_R1CS_PINS[circuit] ?? ''}`
|
|
540
|
+
|| !Array.isArray(inputs)
|
|
541
|
+
|| inputs.length !== 6
|
|
542
|
+
|| inputs.some((value) => !HEX_32_RE.test(value))
|
|
543
|
+
) {
|
|
544
|
+
errors.push(makeError('ZK_EVIDENCE_PROOF_INVALID', 'Canonical Groth16 proof, verification-key pin, or public inputs are invalid.'));
|
|
545
|
+
}
|
|
546
|
+
} else if (evidence.proof !== null) {
|
|
547
|
+
errors.push(makeError('ZK_EVIDENCE_PROOF_UNEXPECTED', 'Non-proving lifecycle events must reference predecessor evidence.'));
|
|
548
|
+
}
|
|
549
|
+
if (!options.expectedProgramId) {
|
|
550
|
+
errors.push(makeError('ZK_EVIDENCE_TRUST_REQUIRED', 'Verification requires an expectedProgramId trust anchor.'));
|
|
551
|
+
} else if (evidence.program_id !== options.expectedProgramId) {
|
|
552
|
+
errors.push(makeError('ZK_EVIDENCE_PROGRAM_MISMATCH', 'Evidence program_id does not match the trusted program.'));
|
|
553
|
+
}
|
|
554
|
+
if (!options.expectedCluster) {
|
|
555
|
+
errors.push(makeError('ZK_EVIDENCE_TRUST_REQUIRED', 'Verification requires an expectedCluster trust anchor.'));
|
|
556
|
+
} else if (evidence.cluster !== options.expectedCluster) {
|
|
557
|
+
errors.push(makeError('ZK_EVIDENCE_CLUSTER_MISMATCH', 'Evidence cluster does not match the trusted cluster.'));
|
|
558
|
+
}
|
|
559
|
+
if (!signerIsTrusted(evidence.signer, options.trustedSigners)) {
|
|
560
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_UNTRUSTED', 'Evidence signer is not present in the trusted signer registry.'));
|
|
561
|
+
}
|
|
562
|
+
if (evidence.signer?.signer_domain !== `${evidence.program_id}:${evidence.cluster}`) {
|
|
563
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_DOMAIN_INVALID', 'Evidence signer domain is not bound to program_id and cluster.'));
|
|
564
|
+
}
|
|
565
|
+
if (!finalizedCheckpointMatches(evidence, options.finalizedCheckpoints)) {
|
|
566
|
+
errors.push(makeError('ZK_EVIDENCE_FINALITY_UNTRUSTED', 'Evidence is not bound to a trusted finalized checkpoint.'));
|
|
567
|
+
}
|
|
568
|
+
if (!evidence.state || !HEX_32_RE.test(evidence.state.note_root ?? '') || !HEX_32_RE.test(evidence.state.null_root ?? '')) {
|
|
569
|
+
errors.push(makeError('ZK_EVIDENCE_STATE_INVALID', 'Evidence must bind canonical note_root and null_root values.'));
|
|
570
|
+
}
|
|
571
|
+
if (Array.isArray(inputs) && inputs.length === 6 && (evidence.event === 'action.append' || evidence.event === 'action.revoke')) {
|
|
572
|
+
const append = evidence.event === 'action.append';
|
|
573
|
+
const state = evidence.state ?? {};
|
|
574
|
+
const mode = Number(BigInt(`0x${inputs[5]}`) >> 60n);
|
|
575
|
+
const countsValid = [state.note_count, state.null_count, state.previous_note_count, state.previous_null_count]
|
|
576
|
+
.every((value) => Number.isSafeInteger(value) && value >= 0);
|
|
577
|
+
const transitionValid = state.note_root === inputs[1]
|
|
578
|
+
&& state.null_root === inputs[3]
|
|
579
|
+
&& state.previous_note_root === inputs[0]
|
|
580
|
+
&& state.previous_null_root === inputs[2]
|
|
581
|
+
&& mode === (append ? 0 : 1)
|
|
582
|
+
&& (append ? inputs[4] === '0'.repeat(64) : inputs[4] !== '0'.repeat(64))
|
|
583
|
+
&& (append
|
|
584
|
+
? inputs[2] === inputs[3] && state.note_count === state.previous_note_count + 1 && state.null_count === state.previous_null_count
|
|
585
|
+
: inputs[0] === inputs[1] && state.note_count === state.previous_note_count && state.null_count === state.previous_null_count + 1);
|
|
586
|
+
if (!countsValid || !transitionValid) {
|
|
587
|
+
errors.push(makeError('ZK_EVIDENCE_ACTION_TRANSITION_INVALID', 'Action label, packed mode, nullifier, roots, or count transition is invalid.'));
|
|
588
|
+
}
|
|
589
|
+
} else if (Array.isArray(inputs) && inputs.length === 6 && evidence.event === 'sale.list') {
|
|
590
|
+
const bindings = [['note_root', 0], ['payload_commit_pos', 1], ['cipher_commit', 2], ['key_commit', 3], ['sale_terms_hash', 4], ['envelope_commit', 5]];
|
|
591
|
+
if (bindings.some(([field, index]) => evidence.state?.[field] !== inputs[index])) {
|
|
592
|
+
errors.push(makeError('ZK_EVIDENCE_STATE_BINDING_INVALID', 'Sale state commitments do not match proof public inputs.'));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (!Array.isArray(evidence.predecessor_refs) || evidence.predecessor_refs.some((ref) => !RECEIPT_REF_RE.test(ref))) {
|
|
596
|
+
errors.push(makeError('ZK_EVIDENCE_PREDECESSOR_INVALID', 'predecessor_refs contains an invalid receipt reference.'));
|
|
597
|
+
}
|
|
598
|
+
const { receipt_ref: receiptRef, signature, ...body } = evidence;
|
|
599
|
+
const signedBytes = Buffer.from(canonicalJson(body), 'utf8');
|
|
600
|
+
if (receiptRef !== `zksettle_${sha256(signedBytes)}`) {
|
|
601
|
+
errors.push(makeError('ZK_EVIDENCE_REF_INVALID', 'receipt_ref does not match the canonical signed body.'));
|
|
602
|
+
}
|
|
603
|
+
const publicRaw = Buffer.from(evidence.signer?.public_key ?? '', 'base64');
|
|
604
|
+
if (
|
|
605
|
+
evidence.signer?.algorithm !== 'Ed25519'
|
|
606
|
+
|| publicRaw.length !== 32
|
|
607
|
+
|| evidence.signer?.key_id !== `sha256:${sha256(publicRaw)}`
|
|
608
|
+
) {
|
|
609
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNER_INVALID', 'Evidence signer metadata is invalid.'));
|
|
610
|
+
} else {
|
|
611
|
+
const publicKey = createPublicKey({ key: Buffer.concat([PUBLIC_KEY_PREFIX, publicRaw]), format: 'der', type: 'spki' });
|
|
612
|
+
const signatureBytes = Buffer.from(signature ?? '', 'base64');
|
|
613
|
+
if (signatureBytes.length !== 64 || !verify(null, signedBytes, publicKey, signatureBytes)) {
|
|
614
|
+
errors.push(makeError('ZK_EVIDENCE_SIGNATURE_INVALID', 'Ed25519 evidence signature did not verify.'));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
errors.push(makeError('ZK_EVIDENCE_INVALID', error.message));
|
|
619
|
+
}
|
|
620
|
+
const valid = errors.length === 0;
|
|
621
|
+
return {
|
|
622
|
+
ok: valid,
|
|
623
|
+
preflight_valid: valid,
|
|
624
|
+
proof_valid: null,
|
|
625
|
+
settlement_verified: false,
|
|
626
|
+
receipt_ref: evidence.receipt_ref ?? null,
|
|
627
|
+
event: evidence.event ?? null,
|
|
628
|
+
errors,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function validateZkSettlementEvidenceGraph(receipts) {
|
|
633
|
+
const errors = [];
|
|
634
|
+
if (!Array.isArray(receipts)) {
|
|
635
|
+
return { ok: false, errors: [makeError('ZK_GRAPH_INVALID', 'Settlement evidence graph must be an ordered array.')] };
|
|
636
|
+
}
|
|
637
|
+
const earlier = new Map();
|
|
638
|
+
const transactionKeys = new Set();
|
|
639
|
+
const proofKeys = new Set();
|
|
640
|
+
const requiredPredecessor = {
|
|
641
|
+
'sale.list': 'action.append',
|
|
642
|
+
'sale.claim': 'sale.list',
|
|
643
|
+
'sale.open': 'sale.claim',
|
|
644
|
+
};
|
|
645
|
+
const stateFields = ['note_root', 'null_root', 'note_count', 'null_count'];
|
|
646
|
+
const saleFields = ['sale_terms_hash', 'key_commit', 'envelope_commit', 'sale_receipt_pda'];
|
|
647
|
+
|
|
648
|
+
for (const evidence of receipts) {
|
|
649
|
+
const ref = evidence?.receipt_ref;
|
|
650
|
+
if (typeof ref !== 'string' || earlier.has(ref)) {
|
|
651
|
+
errors.push(makeError('ZK_GRAPH_DUPLICATE_REF', `Duplicate or invalid receipt_ref ${String(ref)}.`));
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const transactionKey = evidence.transaction?.signature;
|
|
655
|
+
if (transactionKey) {
|
|
656
|
+
if (transactionKeys.has(transactionKey)) {
|
|
657
|
+
errors.push(makeError('ZK_GRAPH_TRANSACTION_REPLAY', `Transaction signature ${transactionKey} is reused.`));
|
|
658
|
+
}
|
|
659
|
+
transactionKeys.add(transactionKey);
|
|
660
|
+
}
|
|
661
|
+
if (evidence.proof) {
|
|
662
|
+
const proofKey = canonicalJson({
|
|
663
|
+
circuit: evidence.circuit,
|
|
664
|
+
digest: evidence.proof.digest,
|
|
665
|
+
public_inputs: evidence.proof.public_inputs,
|
|
666
|
+
});
|
|
667
|
+
if (proofKeys.has(proofKey)) {
|
|
668
|
+
errors.push(makeError('ZK_GRAPH_PROOF_REPLAY', `Circuit/proof/public-input tuple is reused by ${ref}.`));
|
|
669
|
+
}
|
|
670
|
+
proofKeys.add(proofKey);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const refs = Array.isArray(evidence.predecessor_refs) ? evidence.predecessor_refs : [];
|
|
674
|
+
const expectedEvent = requiredPredecessor[evidence.event];
|
|
675
|
+
if (expectedEvent && refs.length !== 1) {
|
|
676
|
+
errors.push(makeError('ZK_GRAPH_LINEAGE_REQUIRED', `${evidence.event} requires exactly one ${expectedEvent} predecessor.`));
|
|
677
|
+
} else if ((evidence.event === 'action.append' || evidence.event === 'action.revoke') && refs.length > 1) {
|
|
678
|
+
errors.push(makeError('ZK_GRAPH_LINEAGE_INVALID', `${evidence.event} accepts at most one prior action predecessor.`));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
for (const predecessorRef of refs) {
|
|
682
|
+
const predecessor = earlier.get(predecessorRef);
|
|
683
|
+
if (!predecessor) {
|
|
684
|
+
errors.push(makeError('ZK_GRAPH_PREDECESSOR_ORDER', `Predecessor ${predecessorRef} is missing or does not appear earlier.`));
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const compatible = expectedEvent
|
|
688
|
+
? predecessor.event === expectedEvent
|
|
689
|
+
: (evidence.event === 'action.append' || evidence.event === 'action.revoke')
|
|
690
|
+
&& (predecessor.event === 'action.append' || predecessor.event === 'action.revoke');
|
|
691
|
+
if (!compatible) {
|
|
692
|
+
errors.push(makeError('ZK_GRAPH_EVENT_INVALID', `${evidence.event} cannot descend from ${predecessor.event}.`));
|
|
693
|
+
}
|
|
694
|
+
if (
|
|
695
|
+
evidence.program_id !== predecessor.program_id
|
|
696
|
+
|| evidence.cluster !== predecessor.cluster
|
|
697
|
+
|| evidence.signer?.signer_domain !== predecessor.signer?.signer_domain
|
|
698
|
+
) {
|
|
699
|
+
errors.push(makeError('ZK_GRAPH_DOMAIN_MISMATCH', 'Lineage must remain in one program, cluster, and signer trust domain.'));
|
|
700
|
+
}
|
|
701
|
+
if (evidence.event === 'action.append' || evidence.event === 'action.revoke') {
|
|
702
|
+
const continuous = predecessor.state?.note_root === evidence.state?.previous_note_root
|
|
703
|
+
&& predecessor.state?.null_root === evidence.state?.previous_null_root
|
|
704
|
+
&& predecessor.state?.note_count === evidence.state?.previous_note_count
|
|
705
|
+
&& predecessor.state?.null_count === evidence.state?.previous_null_count;
|
|
706
|
+
if (!continuous) errors.push(makeError('ZK_GRAPH_STATE_MISMATCH', 'Action predecessor does not reach the child input state.'));
|
|
707
|
+
} else {
|
|
708
|
+
if (!stateFields.every((field) => predecessor.state?.[field] === evidence.state?.[field])) {
|
|
709
|
+
errors.push(makeError('ZK_GRAPH_STATE_MISMATCH', `${predecessor.event} state does not continue into ${evidence.event}.`));
|
|
710
|
+
}
|
|
711
|
+
if (
|
|
712
|
+
(evidence.event === 'sale.claim' || evidence.event === 'sale.open')
|
|
713
|
+
&& !saleFields.every((field) => predecessor.state?.[field] === evidence.state?.[field])
|
|
714
|
+
) {
|
|
715
|
+
errors.push(makeError('ZK_GRAPH_SALE_MISMATCH', `Sale commitments changed between ${predecessor.event} and ${evidence.event}.`));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
earlier.set(ref, evidence);
|
|
720
|
+
}
|
|
721
|
+
return { ok: errors.length === 0, errors };
|
|
722
|
+
}
|