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,783 @@
|
|
|
1
|
+
// sale-witness.mjs — client-side witness builder for
|
|
2
|
+
// circuits/sealed_sale_release.circom — the ZKCP "Sealed Sale" release proof.
|
|
3
|
+
//
|
|
4
|
+
// Binding spec (SaleCircuit-confirmed via hub, 2026-08-23): a Groth16 proof
|
|
5
|
+
// that a Poseidon-CTR ciphertext (committed in a note of the shared note tree)
|
|
6
|
+
// decrypts under a committed data-encryption key (DEK), AND that the same DEK
|
|
7
|
+
// is wrapped to the buyer's BabyJubJub public key in a recipient-bound
|
|
8
|
+
// envelope. Payment releases ONLY against this proof; no key material ever
|
|
9
|
+
// touches the chain.
|
|
10
|
+
//
|
|
11
|
+
// Circuit legs mirrored here EXACTLY:
|
|
12
|
+
// Leg A — note membership: note = Poseidon3(ownerKey, rho, payloadCommitPos)
|
|
13
|
+
// ∈ noteRoot (shared note tree, D=20, LSB-first path bits).
|
|
14
|
+
// Leg B — encryption consistency (Poseidon-CTR stream cipher, field add):
|
|
15
|
+
// ks_i = Poseidon4(dekLo, dekHi, nonce, i)
|
|
16
|
+
// c_i = p_i + ks_i (mod r; circuit enforces c_i - ks_i = p_i)
|
|
17
|
+
// payloadCommitPos = fold(acc=0; Poseidon2(acc, p_i)) (PUBLIC)
|
|
18
|
+
// cipherCommit = fold(acc=0; Poseidon2(acc, c_i)) (PUBLIC —
|
|
19
|
+
// fund-loss fix: the delivered ciphertext is
|
|
20
|
+
// bound on-chain, so junk delivery is provable
|
|
21
|
+
// before the buyer pays)
|
|
22
|
+
// Leg C — keyCommit = Poseidon2(dekLo, dekHi); dekLo/dekHi each < 2^128.
|
|
23
|
+
// Leg D — recipient-bound envelope (BabyJubJub on BN254, circomlib babyjub):
|
|
24
|
+
// buyer pubkey B on-curve (BabyCheck equation) AND not small-order
|
|
25
|
+
// (B8 = 8·B via three doublings, constrain B8.x ≠ 0 — the only
|
|
26
|
+
// on-curve points with x=0 are identity (0,1) and order-2 (0,-1),
|
|
27
|
+
// so x≠0 ⟺ 8·B ≠ identity ⟺ B not in the 8-torsion).
|
|
28
|
+
// epk = esk·Base8, shared = esk·B (esk < 2^253, Num2Bits(253))
|
|
29
|
+
// ss = Poseidon2(shared.x, shared.y)
|
|
30
|
+
// encDEK_lo = dekLo + Poseidon2(ss, 0) (mod r)
|
|
31
|
+
// encDEK_hi = dekHi + Poseidon2(ss, 1) (mod r)
|
|
32
|
+
// envelopeCommit = Poseidon4(epk.x, epk.y, encDEK_lo, encDEK_hi) (PUBLIC)
|
|
33
|
+
// Leg E — saleTermsHash = Poseidon9(DOMAIN_SALE, sellerLo, sellerHi,
|
|
34
|
+
// buyerPubX, buyerPubY, mintLo, mintHi, price, expirySlot);
|
|
35
|
+
// seller/mint are Solana pubkeys split LE 128-bit limbs;
|
|
36
|
+
// price < 2^64; expirySlot < 2^48. The buyer pubkey limbs are
|
|
37
|
+
// bound into the terms (fund-loss fix, Main 2026-08-23): without
|
|
38
|
+
// them a seller could claim a buyer's escrow with a receipt
|
|
39
|
+
// wrapped to a colluder under the SAME terms hash.
|
|
40
|
+
//
|
|
41
|
+
// Public signals (exact order/names): noteRoot, payloadCommitPos,
|
|
42
|
+
// cipherCommit, keyCommit, saleTermsHash, envelopeCommit.
|
|
43
|
+
// Private signals: ownerKey, rho, notePathElements[20], notePathIndices[20]
|
|
44
|
+
// (LSB-first), plaintext[32], ciphertext[32], nonce, dekLo, dekHi,
|
|
45
|
+
// buyerPubX, buyerPubY, esk, sellerLo, sellerHi, mintLo, mintHi, price,
|
|
46
|
+
// expirySlot.
|
|
47
|
+
//
|
|
48
|
+
// All field elements are emitted as DECIMAL STRINGS (snarkjs JSON convention).
|
|
49
|
+
|
|
50
|
+
import { randomBytes } from "node:crypto";
|
|
51
|
+
|
|
52
|
+
import { buildBabyjub } from "circomlibjs";
|
|
53
|
+
import bs58 from "bs58";
|
|
54
|
+
|
|
55
|
+
import { FIELD_R, NoteTree, poseidon } from "./zk-tree.mjs";
|
|
56
|
+
|
|
57
|
+
/** BabyJubJub curve over the BN254 scalar field (circomlibjs). Coordinates
|
|
58
|
+
* live in Fr — identical to the circuit's field, so no modulus translation
|
|
59
|
+
* is ever needed. */
|
|
60
|
+
const babyJub = await buildBabyjub();
|
|
61
|
+
const JF = babyJub.F;
|
|
62
|
+
|
|
63
|
+
/** Note tree depth (matches the shared note tree / NoteTree default). */
|
|
64
|
+
export const NOTE_DEPTH = 20;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* DOMAIN_SALE = 0x656e69676d615f73616c65 ("enigma_sale" ASCII, 11 bytes,
|
|
68
|
+
* big-endian) = 122622912455040968075340901. Confirmed with SaleCircuit.
|
|
69
|
+
*/
|
|
70
|
+
export const DOMAIN_SALE = BigInt("0x656e69676d615f73616c65");
|
|
71
|
+
/** Domain separator for the salted plaintext-fold init ("enigma_psalt" ASCII BE). */
|
|
72
|
+
export const DOMAIN_PSALT = BigInt("0x656e69676d615f7073616c74");
|
|
73
|
+
|
|
74
|
+
/** Public signal names in exact on-chain/circuit order (6 publics, IC 7).
|
|
75
|
+
* cipherCommit sits at index 2 (fund-loss fix, Main 2026-08-23): the buyer
|
|
76
|
+
* verifies the delivered ciphertext folds to this PUBLIC commitment before
|
|
77
|
+
* paying — that check is the junk-delivery defense. */
|
|
78
|
+
export const SALE_PUBLIC_SIGNALS = Object.freeze([
|
|
79
|
+
"noteRoot",
|
|
80
|
+
"payloadCommitPos",
|
|
81
|
+
"cipherCommit",
|
|
82
|
+
"keyCommit",
|
|
83
|
+
"saleTermsHash",
|
|
84
|
+
"envelopeCommit",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** Fixed plaintext/ciphertext chunk count (circuit shape is fixed at n=32). */
|
|
88
|
+
export const CHUNK_COUNT = 32;
|
|
89
|
+
/** Bytes per chunk: 31-byte little-endian limbs (< 2^248 < r, always canonical). */
|
|
90
|
+
export const CHUNK_BYTES = 31;
|
|
91
|
+
/** Maximum plaintext size: 32 chunks × 31 bytes. */
|
|
92
|
+
export const PAYLOAD_CAPACITY = CHUNK_COUNT * CHUNK_BYTES; // 992
|
|
93
|
+
|
|
94
|
+
const R = FIELD_R; // BN254 scalar field modulus
|
|
95
|
+
const TWO_64 = 1n << 64n;
|
|
96
|
+
const TWO_128 = 1n << 128n;
|
|
97
|
+
const TWO_48 = 1n << 48n;
|
|
98
|
+
const TWO_248 = 1n << 248n;
|
|
99
|
+
const TWO_253 = 1n << 253n;
|
|
100
|
+
|
|
101
|
+
const SUB_ORDER = BigInt(babyJub.subOrder.toString()); // prime subgroup order
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Byte/limb helpers
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** Little-endian bytes → bigint. */
|
|
108
|
+
export function bytesToBigIntLE(bytes) {
|
|
109
|
+
let v = 0n;
|
|
110
|
+
for (let i = bytes.length - 1; i >= 0; i--) v = (v << 8n) | BigInt(bytes[i]);
|
|
111
|
+
return v;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** bigint → exactly `len` little-endian bytes. */
|
|
115
|
+
export function bigIntToBytesLE(value, len) {
|
|
116
|
+
let v = BigInt(value);
|
|
117
|
+
if (v < 0n) throw new RangeError("bigIntToBytesLE: negative value");
|
|
118
|
+
const out = new Uint8Array(len);
|
|
119
|
+
for (let i = 0; i < len; i++) {
|
|
120
|
+
out[i] = Number(v & 0xffn);
|
|
121
|
+
v >>= 8n;
|
|
122
|
+
}
|
|
123
|
+
if (v !== 0n) throw new RangeError(`bigIntToBytesLE: value does not fit in ${len} bytes`);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Plaintext bytes → exactly CHUNK_COUNT field chunks: consecutive 31-byte
|
|
129
|
+
* little-endian limbs, zero-padded. Every chunk < 2^248 < r (canonical).
|
|
130
|
+
* @param {Uint8Array} bytes at most PAYLOAD_CAPACITY (992) bytes
|
|
131
|
+
* @returns {bigint[]} length-CHUNK_COUNT array
|
|
132
|
+
*/
|
|
133
|
+
export function bytesToChunks(bytes) {
|
|
134
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
135
|
+
throw new TypeError("bytesToChunks: expected a Uint8Array");
|
|
136
|
+
}
|
|
137
|
+
if (bytes.length > PAYLOAD_CAPACITY) {
|
|
138
|
+
throw new RangeError(
|
|
139
|
+
`payload too large: ${bytes.length} bytes > capacity ${PAYLOAD_CAPACITY}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const chunks = new Array(CHUNK_COUNT);
|
|
143
|
+
for (let i = 0; i < CHUNK_COUNT; i++) {
|
|
144
|
+
chunks[i] = bytesToBigIntLE(bytes.subarray(i * CHUNK_BYTES, (i + 1) * CHUNK_BYTES));
|
|
145
|
+
}
|
|
146
|
+
return chunks;
|
|
147
|
+
}
|
|
148
|
+
/** Alias kept for the spec wording (chunksFromBytes ≡ bytesToChunks). */
|
|
149
|
+
export const chunksFromBytes = bytesToChunks;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Field chunks → bytes (inverse of bytesToChunks). Because chunks are
|
|
153
|
+
* zero-padded, the ORIGINAL byte length must be supplied to round-trip
|
|
154
|
+
* plaintexts with trailing 0x00 bytes; defaults to the full capacity.
|
|
155
|
+
* @param {bigint[]|string[]|number[]} chunks exactly CHUNK_COUNT limbs, each < 2^248
|
|
156
|
+
* @param {number} [byteLength] bytes to emit (default PAYLOAD_CAPACITY)
|
|
157
|
+
* @returns {Uint8Array}
|
|
158
|
+
*/
|
|
159
|
+
export function chunksToBytes(chunks, byteLength = PAYLOAD_CAPACITY) {
|
|
160
|
+
if (!Array.isArray(chunks) || chunks.length !== CHUNK_COUNT) {
|
|
161
|
+
throw new TypeError(`chunksToBytes: expected ${CHUNK_COUNT} chunks`);
|
|
162
|
+
}
|
|
163
|
+
if (!Number.isInteger(byteLength) || byteLength < 0 || byteLength > PAYLOAD_CAPACITY) {
|
|
164
|
+
throw new RangeError(`chunksToBytes: byteLength ${byteLength} out of [0, ${PAYLOAD_CAPACITY}]`);
|
|
165
|
+
}
|
|
166
|
+
const out = new Uint8Array(PAYLOAD_CAPACITY);
|
|
167
|
+
for (let i = 0; i < CHUNK_COUNT; i++) {
|
|
168
|
+
const c = BigInt(chunks[i]);
|
|
169
|
+
if (c < 0n || c >= TWO_248) {
|
|
170
|
+
throw new RangeError(`chunksToBytes: chunk ${i} outside [0, 2^248)`);
|
|
171
|
+
}
|
|
172
|
+
out.set(bigIntToBytesLE(c, CHUNK_BYTES), i * CHUNK_BYTES);
|
|
173
|
+
}
|
|
174
|
+
return out.subarray(0, byteLength);
|
|
175
|
+
}
|
|
176
|
+
/** Alias kept for the spec wording (bytesFromChunks ≡ chunksToBytes). */
|
|
177
|
+
export const bytesFromChunks = chunksToBytes;
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// BabyJubJub helpers (circomlib babyjub — BabyJubJub on BN254; the curve base
|
|
181
|
+
// field IS the circuit scalar field, so coordinates are circuit-native).
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/** Internal: normalize a point given as {x,y} (bigint|string|number) or [x,y]. */
|
|
185
|
+
function toPoint(p, what = "point") {
|
|
186
|
+
let x;
|
|
187
|
+
let y;
|
|
188
|
+
if (Array.isArray(p)) {
|
|
189
|
+
[x, y] = p;
|
|
190
|
+
} else if (p && typeof p === "object") {
|
|
191
|
+
({ x, y } = p);
|
|
192
|
+
}
|
|
193
|
+
if (x === undefined || y === undefined) {
|
|
194
|
+
throw new TypeError(`${what}: expected {x, y} or [x, y]`);
|
|
195
|
+
}
|
|
196
|
+
return [BigInt(x), BigInt(y)];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Encode a BabyJub point as decimal-string coordinates.
|
|
201
|
+
* @param {bigint[]|{x:bigint,y:bigint}} p
|
|
202
|
+
* @returns {{x: string, y: string}}
|
|
203
|
+
*/
|
|
204
|
+
export function encodePoint(p) {
|
|
205
|
+
const [x, y] = Array.isArray(p) ? p.map((c) => JF.toObject(c)) : toPoint(p);
|
|
206
|
+
return { x: BigInt(x).toString(10), y: BigInt(y).toString(10) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Decode decimal-string coordinates → [x, y] bigints. Does NOT validate
|
|
211
|
+
* curve membership (use validateBuyerPublicKey for that).
|
|
212
|
+
*/
|
|
213
|
+
export function decodePoint(p) {
|
|
214
|
+
return toPoint(p);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Buyer public-key validation matching the circuit EXACTLY:
|
|
219
|
+
* 1. coordinates canonical (< r — circuit signals are field elements);
|
|
220
|
+
* 2. on-curve via the BabyJubJub equation (circomlib BabyCheck:
|
|
221
|
+
* a·x² + y² = 1 + d·x²·y² with a=168700, d=168696);
|
|
222
|
+
* 3. not small-order: B8 = 8·B must have B8.x ≠ 0. The only on-curve
|
|
223
|
+
* points with x = 0 are identity (0,1) and the order-2 point (0,−1);
|
|
224
|
+
* since the curve has no order-16 points, 8·B ∈ {(0,1),(0,−1)} iff B
|
|
225
|
+
* lies in the 8-torsion — so x ≠ 0 ⟺ B is not small-order.
|
|
226
|
+
* Throws on any failure; returns the point as [x, y] bigints on success.
|
|
227
|
+
*/
|
|
228
|
+
export function validateBuyerPublicKey(pub, what = "buyerPub") {
|
|
229
|
+
const [x, y] = toPoint(pub, what);
|
|
230
|
+
if (x < 0n || x >= R || y < 0n || y >= R) {
|
|
231
|
+
throw new RangeError(`${what}: coordinates must be canonical field elements (< r)`);
|
|
232
|
+
}
|
|
233
|
+
const P = [JF.e(x), JF.e(y)];
|
|
234
|
+
if (!babyJub.inCurve(P)) {
|
|
235
|
+
throw new Error(`${what}: point is not on the BabyJubJub curve`);
|
|
236
|
+
}
|
|
237
|
+
const P8 = babyJub.mulPointEscalar(P, 8n);
|
|
238
|
+
if (JF.isZero(P8[0])) {
|
|
239
|
+
throw new Error(`${what}: small-order point rejected (8·B has x = 0)`);
|
|
240
|
+
}
|
|
241
|
+
return [x, y];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate a buyer BabyJubJub key pair.
|
|
246
|
+
* @returns {{ secret: bigint, publicKey: {x: bigint, y: bigint} }}
|
|
247
|
+
* secret ∈ [1, subOrder); publicKey = secret·Base8.
|
|
248
|
+
*/
|
|
249
|
+
export function generateBuyerKeyPair() {
|
|
250
|
+
let secret = 0n;
|
|
251
|
+
while (secret === 0n) {
|
|
252
|
+
secret = bytesToBigIntLE(randomBytes(32)) % SUB_ORDER;
|
|
253
|
+
}
|
|
254
|
+
const P = babyJub.mulPointEscalar(babyJub.Base8, secret);
|
|
255
|
+
return { secret, publicKey: { x: JF.toObject(P[0]), y: JF.toObject(P[1]) } };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Buyer-side ECDH: shared = buyerSecret · epk; ss = Poseidon2(shared.x, shared.y).
|
|
260
|
+
* Validates epk with the same rules the circuit applies to the buyer key.
|
|
261
|
+
* @returns {{ ss: bigint, shared: {x: bigint, y: bigint} }}
|
|
262
|
+
*/
|
|
263
|
+
export function deriveSharedSecret(buyerSecret, epk) {
|
|
264
|
+
const [x, y] = validateBuyerPublicKey(epk, "epk");
|
|
265
|
+
const P = babyJub.mulPointEscalar([JF.e(x), JF.e(y)], BigInt(buyerSecret));
|
|
266
|
+
const shared = { x: JF.toObject(P[0]), y: JF.toObject(P[1]) };
|
|
267
|
+
return { ss: poseidon([shared.x, shared.y]), shared };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Poseidon-CTR stream cipher (field-add; mirrors circuit Leg B)
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/** Keystream chunk: ks_i = Poseidon4(dekLo, dekHi, nonce, i). */
|
|
275
|
+
export function keystreamChunk(dekLo, dekHi, nonce, i) {
|
|
276
|
+
return poseidon([BigInt(dekLo), BigInt(dekHi), BigInt(nonce), BigInt(i)]);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Encrypt plaintext chunks: c_i = p_i + Poseidon4(dekLo, dekHi, nonce, i) (mod r).
|
|
281
|
+
* @param {bigint[]|string[]|number[]} plaintextChunks CHUNK_COUNT limbs
|
|
282
|
+
* @returns {bigint[]} CHUNK_COUNT ciphertext chunks
|
|
283
|
+
*/
|
|
284
|
+
export function encryptPayload(plaintextChunks, dekLo, dekHi, nonce) {
|
|
285
|
+
if (!Array.isArray(plaintextChunks) || plaintextChunks.length !== CHUNK_COUNT) {
|
|
286
|
+
throw new TypeError(`encryptPayload: expected ${CHUNK_COUNT} plaintext chunks`);
|
|
287
|
+
}
|
|
288
|
+
const out = new Array(CHUNK_COUNT);
|
|
289
|
+
for (let i = 0; i < CHUNK_COUNT; i++) {
|
|
290
|
+
out[i] = (BigInt(plaintextChunks[i]) + keystreamChunk(dekLo, dekHi, nonce, i)) % R;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Decrypt ciphertext chunks: p_i = c_i − Poseidon4(dekLo, dekHi, nonce, i) (mod r).
|
|
297
|
+
* @param {bigint[]|string[]|number[]} ciphertextChunks CHUNK_COUNT limbs
|
|
298
|
+
* @returns {bigint[]} CHUNK_COUNT plaintext chunks
|
|
299
|
+
*/
|
|
300
|
+
export function decryptPayload(ciphertextChunks, dekLo, dekHi, nonce) {
|
|
301
|
+
if (!Array.isArray(ciphertextChunks) || ciphertextChunks.length !== CHUNK_COUNT) {
|
|
302
|
+
throw new TypeError(`decryptPayload: expected ${CHUNK_COUNT} ciphertext chunks`);
|
|
303
|
+
}
|
|
304
|
+
const out = new Array(CHUNK_COUNT);
|
|
305
|
+
for (let i = 0; i < CHUNK_COUNT; i++) {
|
|
306
|
+
out[i] = (BigInt(ciphertextChunks[i]) - keystreamChunk(dekLo, dekHi, nonce, i)) % R;
|
|
307
|
+
if (out[i] < 0n) out[i] += R;
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Chunk-commitment fold: acc_0 = 0; acc_{i+1} = Poseidon2(acc_i, chunk_i).
|
|
314
|
+
* Used for BOTH payloadCommitPos (plaintext fold, public) and cipherCommit
|
|
315
|
+
* (ciphertext fold, private).
|
|
316
|
+
*/
|
|
317
|
+
/** DEK-blinded, domain-separated plaintext fold (mirrors circuit Leg B init).
|
|
318
|
+
* payloadCommitPos = fold(Poseidon2, Poseidon3(DOMAIN_PSALT, dekLo, dekHi), chunks).
|
|
319
|
+
* The seed is secret until the paid claim reveal, so neither observers nor the
|
|
320
|
+
* pre-payment buyer can dictionary-test the public digest. */
|
|
321
|
+
export function foldChunksBlind(chunks, dekLo, dekHi) {
|
|
322
|
+
let acc = poseidon([DOMAIN_PSALT, BigInt(dekLo), BigInt(dekHi)]);
|
|
323
|
+
for (const c of chunks) acc = poseidon([acc, BigInt(c)]);
|
|
324
|
+
return acc;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function foldChunks(chunks) {
|
|
328
|
+
if (!Array.isArray(chunks) || chunks.length !== CHUNK_COUNT) {
|
|
329
|
+
throw new TypeError(`foldChunks: expected ${CHUNK_COUNT} chunks`);
|
|
330
|
+
}
|
|
331
|
+
let acc = 0n;
|
|
332
|
+
for (const c of chunks) acc = poseidon([acc, BigInt(c)]);
|
|
333
|
+
return acc;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Poseidon commitments (mirror the circuit exactly)
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
/** keyCommit = Poseidon2(dekLo, dekHi). */
|
|
341
|
+
export function computeKeyCommit(dekLo, dekHi) {
|
|
342
|
+
return poseidon([BigInt(dekLo), BigInt(dekHi)]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Sale note leaf = Poseidon3(ownerKey, rho, payloadCommitPos) — insert into NoteTree. */
|
|
346
|
+
export function computeSaleNote(ownerKey, rho, payloadCommitPos) {
|
|
347
|
+
return poseidon([BigInt(ownerKey), BigInt(rho), BigInt(payloadCommitPos)]);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** base58 → exactly `len` bytes (Solana pubkey / fixed-size buffer). */
|
|
351
|
+
function decodeBase58Fixed(b58, len, what) {
|
|
352
|
+
if (typeof b58 !== "string" || b58.length === 0) {
|
|
353
|
+
throw new TypeError(`${what}: expected a non-empty base58 string`);
|
|
354
|
+
}
|
|
355
|
+
const bytes = bs58.decode(b58);
|
|
356
|
+
if (bytes.length !== len) {
|
|
357
|
+
throw new Error(`${what}: expected ${len} bytes, got ${bytes.length}`);
|
|
358
|
+
}
|
|
359
|
+
return bytes;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* saleTermsHash = Poseidon9(DOMAIN_SALE, sellerLo, sellerHi, buyerPubX,
|
|
364
|
+
* buyerPubY, mintLo, mintHi, price, expirySlot). seller/mint: base58 32-byte
|
|
365
|
+
* Solana pubkeys split into little-endian 128-bit limbs; buyerPub: BabyJub
|
|
366
|
+
* public key (validated exactly as the circuit validates it); price: u64
|
|
367
|
+
* lamports; expirySlot < 2^48.
|
|
368
|
+
* @returns {{ saleTermsHash: bigint, sellerLo: bigint, sellerHi: bigint,
|
|
369
|
+
* buyerPubX: bigint, buyerPubY: bigint, mintLo: bigint, mintHi: bigint,
|
|
370
|
+
* price: bigint, expirySlot: bigint }}
|
|
371
|
+
*/
|
|
372
|
+
export function computeSaleTermsHash({ sellerPubkey, buyerPub, mint, priceLamports, expirySlot, payloadLen }) {
|
|
373
|
+
const sBytes = decodeBase58Fixed(sellerPubkey, 32, "sellerPubkey");
|
|
374
|
+
const mBytes = decodeBase58Fixed(mint, 32, "mint");
|
|
375
|
+
const [buyerPubX, buyerPubY] = validateBuyerPublicKey(buyerPub);
|
|
376
|
+
const price = BigInt(priceLamports);
|
|
377
|
+
const expiry = BigInt(expirySlot);
|
|
378
|
+
if (price < 0n || price >= TWO_64) {
|
|
379
|
+
throw new RangeError(`priceLamports ${price} out of u64 range`);
|
|
380
|
+
}
|
|
381
|
+
if (expiry < 0n || expiry >= TWO_48) {
|
|
382
|
+
throw new RangeError(`expirySlot ${expiry} out of 48-bit range`);
|
|
383
|
+
}
|
|
384
|
+
if (payloadLen === undefined || payloadLen === null) {
|
|
385
|
+
throw new TypeError("computeSaleTermsHash: payloadLen required (bound into the terms)");
|
|
386
|
+
}
|
|
387
|
+
const length = BigInt(payloadLen);
|
|
388
|
+
if (length < 1n || length > BigInt(PAYLOAD_CAPACITY)) {
|
|
389
|
+
throw new RangeError(`payloadLen ${length} out of range 1..${PAYLOAD_CAPACITY}`);
|
|
390
|
+
}
|
|
391
|
+
const limbs = {
|
|
392
|
+
sellerLo: bytesToBigIntLE(sBytes.subarray(0, 16)),
|
|
393
|
+
sellerHi: bytesToBigIntLE(sBytes.subarray(16, 32)),
|
|
394
|
+
buyerPubX,
|
|
395
|
+
buyerPubY,
|
|
396
|
+
mintLo: bytesToBigIntLE(mBytes.subarray(0, 16)),
|
|
397
|
+
mintHi: bytesToBigIntLE(mBytes.subarray(16, 32)),
|
|
398
|
+
price,
|
|
399
|
+
expirySlot: expiry,
|
|
400
|
+
payloadLen: length,
|
|
401
|
+
};
|
|
402
|
+
const saleTermsHash = poseidon([
|
|
403
|
+
DOMAIN_SALE,
|
|
404
|
+
limbs.sellerLo,
|
|
405
|
+
limbs.sellerHi,
|
|
406
|
+
limbs.buyerPubX,
|
|
407
|
+
limbs.buyerPubY,
|
|
408
|
+
limbs.mintLo,
|
|
409
|
+
limbs.mintHi,
|
|
410
|
+
limbs.price,
|
|
411
|
+
limbs.expirySlot,
|
|
412
|
+
limbs.payloadLen,
|
|
413
|
+
]);
|
|
414
|
+
return { saleTermsHash, ...limbs };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Envelope (Leg D) — seller-side wrap, buyer-side unwrap
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
/** Field add mod r. */
|
|
422
|
+
const fadd = (a, b) => (BigInt(a) + BigInt(b)) % R;
|
|
423
|
+
/** Field sub mod r. */
|
|
424
|
+
const fsub = (a, b) => {
|
|
425
|
+
const v = (BigInt(a) - BigInt(b)) % R;
|
|
426
|
+
return v < 0n ? v + R : v;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Seller-side DEK wrap to the buyer's public key.
|
|
431
|
+
* @returns {{ epk: {x: bigint, y: bigint}, encDEK_lo: bigint, encDEK_hi: bigint,
|
|
432
|
+
* envelopeCommit: bigint, ss: bigint }}
|
|
433
|
+
*/
|
|
434
|
+
function wrapDek({ dekLo, dekHi, nonce, buyerPub, esk }) {
|
|
435
|
+
const [bx, by] = validateBuyerPublicKey(buyerPub);
|
|
436
|
+
if (esk < 0n || esk >= TWO_253) {
|
|
437
|
+
throw new RangeError(`esk out of range: circuit constrains Num2Bits(253)`);
|
|
438
|
+
}
|
|
439
|
+
if (esk % SUB_ORDER === 0n) {
|
|
440
|
+
// esk = 0 or esk = subOrder → epk = identity → shared secret publicly
|
|
441
|
+
// derivable → envelope opens for ANYONE. The circuit rejects this too
|
|
442
|
+
// (epk.x ≠ 0), fail here first with a clear reason.
|
|
443
|
+
throw new RangeError('esk must not be 0 mod subOrder (epk would be the identity point)');
|
|
444
|
+
}
|
|
445
|
+
if (nonce === undefined || nonce === null) {
|
|
446
|
+
throw new TypeError('wrapDek: nonce required — it is bound into envelopeCommit');
|
|
447
|
+
}
|
|
448
|
+
const B = [JF.e(bx), JF.e(by)];
|
|
449
|
+
const epkP = babyJub.mulPointEscalar(babyJub.Base8, esk);
|
|
450
|
+
const sharedP = babyJub.mulPointEscalar(B, esk);
|
|
451
|
+
const shared = { x: JF.toObject(sharedP[0]), y: JF.toObject(sharedP[1]) };
|
|
452
|
+
const ss = poseidon([shared.x, shared.y]);
|
|
453
|
+
const encDEK_lo = fadd(dekLo, poseidon([ss, 0n]));
|
|
454
|
+
const encDEK_hi = fadd(dekHi, poseidon([ss, 1n]));
|
|
455
|
+
const epk = { x: JF.toObject(epkP[0]), y: JF.toObject(epkP[1]) };
|
|
456
|
+
// Poseidon5 binds the CTR nonce: the claim-time reveal is the only (epk,
|
|
457
|
+
// encDEK, nonce) triple the proof could have committed to.
|
|
458
|
+
const envelopeCommit = poseidon([epk.x, epk.y, encDEK_lo, encDEK_hi, BigInt(nonce)]);
|
|
459
|
+
return { epk, encDEK_lo, encDEK_hi, envelopeCommit, ss };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Buyer-side DEK unwrap: shared = buyerSecret·epk; ss = Poseidon2(shared.x,
|
|
464
|
+
* shared.y); dekLo = encDEK_lo − Poseidon2(ss, 0); dekHi = encDEK_hi −
|
|
465
|
+
* Poseidon2(ss, 1) (all mod r).
|
|
466
|
+
* @param {bigint|string} buyerSecret buyer's BabyJub secret scalar
|
|
467
|
+
* @param {{x,y}|[x,y]} epk ephemeral public key (validated like the circuit
|
|
468
|
+
* validates the buyer key: on-curve, not small-order)
|
|
469
|
+
* @returns {{ dekLo: bigint, dekHi: bigint, ss: bigint }}
|
|
470
|
+
*/
|
|
471
|
+
export function openSaleEnvelope(buyerSecret, epk, encDEK_lo, encDEK_hi) {
|
|
472
|
+
const { ss } = deriveSharedSecret(buyerSecret, epk);
|
|
473
|
+
const dekLo = fsub(encDEK_lo, poseidon([ss, 0n]));
|
|
474
|
+
const dekHi = fsub(encDEK_hi, poseidon([ss, 1n]));
|
|
475
|
+
return { dekLo, dekHi, ss };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Full buyer-side open. Verifies, IN ORDER:
|
|
480
|
+
* 1. JUNK-DELIVERY DEFENSE: the delivered ciphertext chunks fold to the
|
|
481
|
+
* PUBLIC cipherCommit (public input index 2). This is the check the
|
|
482
|
+
* buyer runs against the on-chain listing BEFORE paying — a seller who
|
|
483
|
+
* delivers bytes that do not fold to the committed cipherCommit is
|
|
484
|
+
* caught here, before any decryption.
|
|
485
|
+
* 2. Envelope unwrap (ECDH) → DEK.
|
|
486
|
+
* 3. Decrypt → assert the plaintext fold equals the listed
|
|
487
|
+
* payloadCommitPos. A third party with the wrong buyer secret derives
|
|
488
|
+
* the wrong DEK → wrong plaintext → this assert fails.
|
|
489
|
+
*
|
|
490
|
+
* @param {object} args
|
|
491
|
+
* @param {bigint|string} args.buyerSecret
|
|
492
|
+
* @param {{x,y}|[x,y]} args.epk
|
|
493
|
+
* @param {bigint|string} args.encDEK_lo
|
|
494
|
+
* @param {bigint|string} args.encDEK_hi
|
|
495
|
+
* @param {bigint[]|string[]} args.ciphertext CHUNK_COUNT delivered ciphertext chunks
|
|
496
|
+
* @param {bigint|string} args.cipherCommit PUBLIC ciphertext fold (on-chain listing)
|
|
497
|
+
* @param {bigint|string} args.nonce Poseidon-CTR nonce
|
|
498
|
+
* @param {bigint|string} args.payloadCommitPos listed plaintext commitment
|
|
499
|
+
* @returns {{ dekLo: bigint, dekHi: bigint, plaintextChunks: bigint[],
|
|
500
|
+
* cipherCommit: bigint, payloadCommitPos: bigint }}
|
|
501
|
+
*/
|
|
502
|
+
export function openAndVerifySale(args) {
|
|
503
|
+
if (args.cipherCommit === undefined || args.cipherCommit === null) {
|
|
504
|
+
throw new TypeError("openAndVerifySale: missing cipherCommit (public ciphertext fold)");
|
|
505
|
+
}
|
|
506
|
+
const cipherCommit = foldChunks(args.ciphertext);
|
|
507
|
+
if (cipherCommit !== BigInt(args.cipherCommit)) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"junk delivery: delivered ciphertext fold != public cipherCommit — do not pay",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const { dekLo, dekHi } = openSaleEnvelope(
|
|
513
|
+
args.buyerSecret,
|
|
514
|
+
args.epk,
|
|
515
|
+
args.encDEK_lo,
|
|
516
|
+
args.encDEK_hi,
|
|
517
|
+
);
|
|
518
|
+
const plaintextChunks = decryptPayload(args.ciphertext, dekLo, dekHi, args.nonce);
|
|
519
|
+
const digest = foldChunksBlind(plaintextChunks, dekLo, dekHi);
|
|
520
|
+
if (digest !== BigInt(args.payloadCommitPos)) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
"sale payload digest mismatch: decrypted blinded fold != listed payloadCommitPos",
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return { dekLo, dekHi, plaintextChunks, cipherCommit, payloadCommitPos: digest };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Witness builder
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
/** Recursively convert bigints → decimal strings for snarkjs input JSON. */
|
|
533
|
+
function toDec(x) {
|
|
534
|
+
if (typeof x === "bigint") return x.toString(10);
|
|
535
|
+
if (Array.isArray(x)) return x.map(toDec);
|
|
536
|
+
if (typeof x === "number") return String(x);
|
|
537
|
+
if (typeof x === "object" && x !== null) {
|
|
538
|
+
return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, toDec(v)]));
|
|
539
|
+
}
|
|
540
|
+
return x;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function need(value, name) {
|
|
544
|
+
if (value === undefined || value === null) {
|
|
545
|
+
throw new TypeError(`buildSaleWitness: missing ${name}`);
|
|
546
|
+
}
|
|
547
|
+
return BigInt(value);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Build the snarkjs input for one sealed-sale release proof.
|
|
552
|
+
*
|
|
553
|
+
* @param {object} args
|
|
554
|
+
* @param {NoteTree} args.noteTree shared note tree at the current on-chain note_root
|
|
555
|
+
* @param {number} args.noteIndex index of the sale note in noteTree
|
|
556
|
+
* @param {bigint|number|string} args.ownerKey note opening
|
|
557
|
+
* @param {bigint|number|string} args.rho note opening
|
|
558
|
+
* @param {Uint8Array} args.plaintextBytes E2EE memory plaintext (≤ 992 bytes)
|
|
559
|
+
* @param {bigint|number|string} args.dekLo DEK low limb (< 2^128)
|
|
560
|
+
* @param {bigint|number|string} args.dekHi DEK high limb (< 2^128)
|
|
561
|
+
* @param {bigint|number|string} args.nonce Poseidon-CTR nonce (field element)
|
|
562
|
+
* @param {{x,y}|[x,y]} args.buyerPub buyer BabyJub public key (decimal strings or bigints)
|
|
563
|
+
* @param {string} args.sellerPubkey seller base58 32B pubkey
|
|
564
|
+
* @param {string} args.mint payment mint, base58 32B pubkey
|
|
565
|
+
* @param {bigint|number|string} args.priceLamports price (u64)
|
|
566
|
+
* @param {bigint|number|string} args.expirySlot escrow expiry slot (< 2^48)
|
|
567
|
+
* @param {bigint|number|string} [args.esk] ephemeral secret (< 2^253); random if omitted
|
|
568
|
+
* @returns {{
|
|
569
|
+
* input: object, // snarkjs input JSON (decimal strings), exact circuit signal names
|
|
570
|
+
* note: bigint, noteRoot: bigint,
|
|
571
|
+
* plaintextChunks: bigint[], ciphertextChunks: bigint[],
|
|
572
|
+
* payloadCommitPos: bigint, cipherCommit: bigint, keyCommit: bigint,
|
|
573
|
+
* saleTermsHash: bigint, envelopeCommit: bigint,
|
|
574
|
+
* epk: {x: bigint, y: bigint}, encDEK_lo: bigint, encDEK_hi: bigint,
|
|
575
|
+
* esk: bigint,
|
|
576
|
+
* sellerLo: bigint, sellerHi: bigint, buyerPubX: bigint, buyerPubY: bigint,
|
|
577
|
+
* mintLo: bigint, mintHi: bigint, price: bigint, expirySlot: bigint,
|
|
578
|
+
* }}
|
|
579
|
+
*/
|
|
580
|
+
export function buildSaleWitness(args) {
|
|
581
|
+
const { noteTree, noteIndex } = args;
|
|
582
|
+
if (!(noteTree instanceof NoteTree)) throw new TypeError("noteTree must be a NoteTree");
|
|
583
|
+
if (!Number.isInteger(noteIndex) || noteIndex < 0 || noteIndex >= noteTree.nextIndex) {
|
|
584
|
+
throw new RangeError(`noteIndex ${noteIndex} not present in note tree`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const ownerKey = need(args.ownerKey, "ownerKey");
|
|
588
|
+
const rho = need(args.rho, "rho");
|
|
589
|
+
const dekLo = need(args.dekLo, "dekLo");
|
|
590
|
+
const dekHi = need(args.dekHi, "dekHi");
|
|
591
|
+
const nonce = need(args.nonce, "nonce");
|
|
592
|
+
|
|
593
|
+
if (dekLo < 0n || dekLo >= TWO_128 || dekHi < 0n || dekHi >= TWO_128) {
|
|
594
|
+
throw new RangeError("dekLo/dekHi must each be < 2^128 (circuit Num2Bits(128))");
|
|
595
|
+
}
|
|
596
|
+
if (nonce < 0n || nonce >= R) throw new RangeError("nonce must be a field element");
|
|
597
|
+
if (ownerKey < 0n || ownerKey >= R || rho < 0n || rho >= R) {
|
|
598
|
+
throw new RangeError("ownerKey/rho must be field elements");
|
|
599
|
+
}
|
|
600
|
+
if (args.plaintextBytes === undefined || args.plaintextBytes === null) {
|
|
601
|
+
throw new TypeError("buildSaleWitness: missing plaintextBytes");
|
|
602
|
+
}
|
|
603
|
+
const payloadLen = BigInt(args.plaintextBytes.length);
|
|
604
|
+
if (payloadLen < 1n || payloadLen > BigInt(PAYLOAD_CAPACITY)) {
|
|
605
|
+
throw new RangeError(
|
|
606
|
+
`buildSaleWitness: payload length ${payloadLen} out of range 1..${PAYLOAD_CAPACITY}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (args.buyerPub === undefined || args.buyerPub === null) {
|
|
610
|
+
throw new TypeError("buildSaleWitness: missing buyerPub");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ---- Leg B: Poseidon-CTR encryption + folds ----
|
|
614
|
+
// The plaintext fold is blinded by a DEK-derived seed — no separate salt.
|
|
615
|
+
const plaintextChunks = bytesToChunks(args.plaintextBytes);
|
|
616
|
+
const ciphertextChunks = encryptPayload(plaintextChunks, dekLo, dekHi, nonce);
|
|
617
|
+
const payloadCommitPos = foldChunksBlind(plaintextChunks, dekLo, dekHi);
|
|
618
|
+
const cipherCommit = foldChunks(ciphertextChunks);
|
|
619
|
+
|
|
620
|
+
// ---- Leg C: key commitment ----
|
|
621
|
+
const keyCommit = computeKeyCommit(dekLo, dekHi);
|
|
622
|
+
|
|
623
|
+
// ---- Leg A: note membership ----
|
|
624
|
+
const note = computeSaleNote(ownerKey, rho, payloadCommitPos);
|
|
625
|
+
if (noteTree.leaves[noteIndex] !== note) {
|
|
626
|
+
throw new Error("note opening does not match the leaf at noteIndex");
|
|
627
|
+
}
|
|
628
|
+
const noteProof = noteTree.getProof(noteIndex);
|
|
629
|
+
const noteRoot = noteTree.root();
|
|
630
|
+
|
|
631
|
+
// ---- Leg D: recipient-bound envelope ----
|
|
632
|
+
let esk;
|
|
633
|
+
if (args.esk !== undefined && args.esk !== null) {
|
|
634
|
+
esk = BigInt(args.esk);
|
|
635
|
+
} else {
|
|
636
|
+
do {
|
|
637
|
+
esk = bytesToBigIntLE(randomBytes(32)) % TWO_253;
|
|
638
|
+
} while (esk % SUB_ORDER === 0n);
|
|
639
|
+
}
|
|
640
|
+
const [buyerPubX, buyerPubY] = validateBuyerPublicKey(args.buyerPub);
|
|
641
|
+
const { epk, encDEK_lo, encDEK_hi, envelopeCommit } = wrapDek({
|
|
642
|
+
dekLo,
|
|
643
|
+
dekHi,
|
|
644
|
+
nonce,
|
|
645
|
+
buyerPub: [buyerPubX, buyerPubY],
|
|
646
|
+
esk,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// ---- Leg E: sale terms (buyer- and length-bound: Poseidon10 over seller,
|
|
650
|
+
// buyer key, mint, price, expiry, payloadLen) ----
|
|
651
|
+
const terms = computeSaleTermsHash({
|
|
652
|
+
sellerPubkey: args.sellerPubkey,
|
|
653
|
+
buyerPub: [buyerPubX, buyerPubY],
|
|
654
|
+
mint: args.mint,
|
|
655
|
+
priceLamports: args.priceLamports,
|
|
656
|
+
expirySlot: args.expirySlot,
|
|
657
|
+
payloadLen,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const input = toDec({
|
|
661
|
+
// public (exact contract order/names)
|
|
662
|
+
noteRoot,
|
|
663
|
+
payloadCommitPos,
|
|
664
|
+
cipherCommit,
|
|
665
|
+
keyCommit,
|
|
666
|
+
saleTermsHash: terms.saleTermsHash,
|
|
667
|
+
envelopeCommit,
|
|
668
|
+
// private
|
|
669
|
+
ownerKey,
|
|
670
|
+
rho,
|
|
671
|
+
notePathElements: noteProof.pathElements,
|
|
672
|
+
notePathIndices: noteProof.pathIndices,
|
|
673
|
+
plaintext: plaintextChunks,
|
|
674
|
+
ciphertext: ciphertextChunks,
|
|
675
|
+
nonce,
|
|
676
|
+
dekLo,
|
|
677
|
+
dekHi,
|
|
678
|
+
buyerPubX,
|
|
679
|
+
buyerPubY,
|
|
680
|
+
esk,
|
|
681
|
+
sellerLo: terms.sellerLo,
|
|
682
|
+
sellerHi: terms.sellerHi,
|
|
683
|
+
mintLo: terms.mintLo,
|
|
684
|
+
mintHi: terms.mintHi,
|
|
685
|
+
price: terms.price,
|
|
686
|
+
expirySlot: terms.expirySlot,
|
|
687
|
+
payloadLen,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
input,
|
|
692
|
+
note,
|
|
693
|
+
noteRoot,
|
|
694
|
+
payloadLen,
|
|
695
|
+
plaintextChunks,
|
|
696
|
+
ciphertextChunks,
|
|
697
|
+
payloadCommitPos,
|
|
698
|
+
cipherCommit,
|
|
699
|
+
keyCommit,
|
|
700
|
+
saleTermsHash: terms.saleTermsHash,
|
|
701
|
+
envelopeCommit,
|
|
702
|
+
epk,
|
|
703
|
+
encDEK_lo,
|
|
704
|
+
encDEK_hi,
|
|
705
|
+
esk,
|
|
706
|
+
sellerLo: terms.sellerLo,
|
|
707
|
+
sellerHi: terms.sellerHi,
|
|
708
|
+
buyerPubX: terms.buyerPubX,
|
|
709
|
+
buyerPubY: terms.buyerPubY,
|
|
710
|
+
mintLo: terms.mintLo,
|
|
711
|
+
mintHi: terms.mintHi,
|
|
712
|
+
price: terms.price,
|
|
713
|
+
expirySlot: terms.expirySlot,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ---------------------------------------------------------------------------
|
|
718
|
+
// Deterministic demo fixture (proof-of-life / setup-sale.mjs)
|
|
719
|
+
// ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Deterministic end-to-end sale fixture: a note tree holding the sale note,
|
|
723
|
+
* a fixed buyer key pair, a fixed DEK/esk, and the full witness input.
|
|
724
|
+
* @returns {Promise<{
|
|
725
|
+
* input: object, // complete circom witness input (decimal strings)
|
|
726
|
+
* expectedPublic: string[], // the 5 public signals, SALE_PUBLIC_SIGNALS order
|
|
727
|
+
* artifacts: object, // full buildSaleWitness result (epk, encDEK_*, ...)
|
|
728
|
+
* buyerSecret: bigint, // demo buyer secret (envelope opening)
|
|
729
|
+
* plaintextBytes: Uint8Array, // demo plaintext
|
|
730
|
+
* }>}
|
|
731
|
+
*/
|
|
732
|
+
export async function buildSaleDemoFixture() {
|
|
733
|
+
const buyer = generateBuyerKeyPairDeterministic();
|
|
734
|
+
const plaintextBytes = new TextEncoder().encode(
|
|
735
|
+
"Enigma sealed-sale demo memory: the cortex remembers what the chain never sees.",
|
|
736
|
+
);
|
|
737
|
+
const dekLo = 0x0de0b6b3a7640000f1f2f3f4f5f6f7f8n; // < 2^128
|
|
738
|
+
const dekHi = 0x00112233445566778899aabbccddeeffn; // < 2^128
|
|
739
|
+
const nonce = 424242424242n;
|
|
740
|
+
const esk = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403n; // < 2^253
|
|
741
|
+
const ownerKey = 11111111111111111111111111111111111111111111111111n;
|
|
742
|
+
const rho = 22222222222222222222222222222222222222222222222222n;
|
|
743
|
+
|
|
744
|
+
// Pre-compute the note so the tree holds the exact leaf the witness opens.
|
|
745
|
+
const plaintextChunks = bytesToChunks(plaintextBytes);
|
|
746
|
+
const payloadCommitPos = foldChunksBlind(plaintextChunks, dekLo, dekHi);
|
|
747
|
+
const noteTree = new NoteTree();
|
|
748
|
+
noteTree.insert(555n); // filler note at index 0
|
|
749
|
+
noteTree.insert(computeSaleNote(ownerKey, rho, payloadCommitPos)); // index 1
|
|
750
|
+
|
|
751
|
+
const result = buildSaleWitness({
|
|
752
|
+
noteTree,
|
|
753
|
+
noteIndex: 1,
|
|
754
|
+
ownerKey,
|
|
755
|
+
rho,
|
|
756
|
+
plaintextBytes,
|
|
757
|
+
dekLo,
|
|
758
|
+
dekHi,
|
|
759
|
+
nonce,
|
|
760
|
+
buyerPub: buyer.publicKey,
|
|
761
|
+
sellerPubkey: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint addr as seller
|
|
762
|
+
mint: "So11111111111111111111111111111111111111112", // wSOL mint
|
|
763
|
+
priceLamports: 2_000_000_000n, // 2 SOL
|
|
764
|
+
expirySlot: 300_000_000n,
|
|
765
|
+
esk,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const expectedPublic = SALE_PUBLIC_SIGNALS.map((name) => result.input[name]);
|
|
769
|
+
return {
|
|
770
|
+
input: result.input,
|
|
771
|
+
expectedPublic,
|
|
772
|
+
artifacts: result,
|
|
773
|
+
buyerSecret: buyer.secret,
|
|
774
|
+
plaintextBytes,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Deterministic buyer key pair for the demo fixture (fixed secret). */
|
|
779
|
+
function generateBuyerKeyPairDeterministic() {
|
|
780
|
+
const secret = 0x0a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252n % SUB_ORDER;
|
|
781
|
+
const P = babyJub.mulPointEscalar(babyJub.Base8, secret);
|
|
782
|
+
return { secret, publicKey: { x: JF.toObject(P[0]), y: JF.toObject(P[1]) } };
|
|
783
|
+
}
|