Geheimnis (German) — secret, mystery.
A CLI template engine that generates a complete ZK-encrypted ERC-721 stack from a single parameter: how many plaintext fields you want to store per token.
Run geheimnis once and get production-ready circom circuits, Groth16 verifier contracts, a Solidity ERC-721, and TypeScript bindings — all wired together and compiled.
This template engine was developed as part of cipher, which was supported by JUST Open Source. The circom circuits rely on the ZK-Kit developed by the Privacy Stewards of Ethereum (PSE).
my-project/
├── circuits/
│ ├── EcdhPoseidonTransfer.circom # transfer proof circuit
│ └── AddNewDataEncrypt.circom # mint / reCipher proof circuit
├── contracts/src/
│ ├── EncryptedERC721.sol # your ERC-721 contract
│ ├── Groth16Verifier_Transfer.sol # auto-generated on-chain verifier
│ ├── Groth16Verifier_AddData.sol # auto-generated on-chain verifier
│ └── BabyJubjub.sol # cosmetic curve point validation
├── bindings/
│ └── index.ts # encrypt / decrypt / proof builders
└── build/
├── *.zkey # proving keys (needed client-side)
└── *_vkey.json # verification keys
Everything is parameterized by N — the number of plaintext field elements per token. All circuit sizes, public signal counts, and contract storage layouts derive automatically from N.
Each token stores N plaintext field elements encrypted on-chain as C = ceil(N/3)×3 + 1 ciphertext uint256 values. Encryption uses Poseidon symmetric cipher keyed via ECDH on the Baby Jubjub curve.
Standard transferFrom is disabled. The only way to transfer a token is through verifiedTransferFrom, which requires a Groth16 proof that the sender:
- Knows their private key
- Correctly decrypted the old ciphertext
- Re-encrypted the same plaintext for the new recipient
Nobody ever sees the plaintext on-chain. The proof system enforces the re-encryption honestly.
After receiving a token, the new owner can call reCipher to re-encrypt under a purely self-derived key, breaking any dependency on the sender's keypair.
# install circom
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
cargo install circomgit clone https://github.com/your-org/geheimnis.git
cd geheimnis
pnpm installRun from the project directory via pnpm:
pnpm geheimnis
pnpm geheimnis ceremony init ./my-project
pnpm geheimnis ceremony contribute ./my-projectOptionally, link it globally so you can run geheimnis from any directory without pnpm:
pnpm build
pnpm link --global
geheimnis
geheimnis ceremony init ./my-projectgeheimnisThe CLI will ask:
| Prompt | Description |
|---|---|
| N | Plaintext field elements per token. Determines circuit size and storage layout. |
| Collection name | Used for the contract name and output directory. |
| Symbol | ERC-721 token symbol. |
| Include minting? | Whether to add public mint logic (price, max supply, trusted minter). |
| Max supply | (if minting enabled) Maximum tokens that can be minted. |
| Mint price | (if minting enabled) Price in ETH per mint. |
| Output directory | Where to write the generated project. |
Geheimnis then:
- Writes all source files
- Compiles both circuits with circom
- Downloads the required Powers-of-Tau file (cached in
~/.geheimnis/ptau/) - Runs the Groth16 trusted setup
- Exports verifier contracts and verification keys
cd my-project/contracts
forge install
forge build
forge script script/Deploy.s.sol --broadcastCopy build/*.zkey and circuits/*.wasm to your front-end for client-side proving.
| N | ptau | Proving | Notes |
|---|---|---|---|
| 1–15 | 2^15 | Browser-safe (~5–10s in a web worker) | Recommended for most use cases |
| 16–30 | 2^16 | Server-side recommended | Still reasonable calldata |
| > 30 | 2^17+ | Slow, large wasm | Not recommended |
The soft limit is N = 30. At N = 30 the total calldata per transaction is roughly 2.5 KB (256 bytes fixed proof + ~2.2 KB public signals), well within RPC limits.
// Anyone can mint by paying mintPrice and supplying a valid proof
contract.mint(proof, pubSignals, to)
// A trusted minter address can mint for free
contract.mintFrom(proof, pubSignals, to)The trusted minter is set at deployment and can be updated by the owner via setMinter(). Useful for a back-end service that generates proofs on behalf of users.
// Only the contract owner can mint
contract.adminMint(proof, pubSignals, to)No price, no supply cap, no trusted minter. Simpler deploy for curated collections.
The default flow uses a single-party trusted setup — reasonable when you already trust the deployer as the contract owner. For higher trust requirements, Geheimnis includes a multi-party ceremony workflow.
The ceremony is secure as long as at least one contributor destroyed their toxic waste after contributing.
When running geheimnis, answer yes to the prompt:
Use multi-party trusted setup? (skips single-party setup — run ceremony init after)
This generates and compiles the circuits but skips step 4. You then run the ceremony commands to produce the final proving keys.
# 1. Deployer: generate project with ceremony mode enabled
geheimnis
# → answer yes to multi-party prompt
# 2. Deployer: initialise the ceremony (creates _0000.zkey files)
geheimnis ceremony init ./my-project
# → enter path to the .ptau file downloaded in step 3 of generation
# 3. Share the ceremony/ directory with each contributor (zip, file share, etc.)
# Each contributor runs:
geheimnis ceremony contribute ./my-project
# → enter your name and optional personal entropy
# → prints a contribution hash — record it and share with the group
# 4. Deployer: finalise once all contributors are done
geheimnis ceremony finalize ./my-project
# → applies a random beacon, exports verifier contracts, verifies the transcript
# 5. Anyone can audit the transcript at any time
geheimnis ceremony verify ./my-projectEach contribution combines the system CSPRNG with optional personal entropy via SHA-256 — neither source alone is sufficient.
Deployer Contributors Deployer
│ │ │
geheimnis contribute ×N ceremony finalize
ceremony init │ │
│ _0001 → _0002 → … beacon → final.zkey
_0000.zkey → export verifiers
→ snarkjs verify
The generated bindings/index.ts exposes helpers for the front-end:
import {
derivePublicKey,
computeSharedKey,
poseidonEncrypt,
poseidonDecrypt,
buildMintInput,
buildTransferInput,
} from './bindings/index.js';
// Derive a Baby Jubjub public key from a private key
const pubKey = derivePublicKey(privateKey);
// Build inputs for a mint proof
const { input, ciphertext } = buildMintInput({ privateKey, message: [1n, 2n, 3n] });
// Build inputs for a transfer proof
const transferInput = buildTransferInput({
senderPrivateKey,
recipientPublicKey,
oldCiphertext,
oldNonce,
message,
});Pass input to snarkjs fullProve() with the appropriate .wasm and .zkey files.
geheimnis (CLI)
├── src/config.ts — parameter derivation (N → C, S_t, S_a, ptauPower)
├── src/generator.ts — circom / Solidity / TS source code generation
├── src/builder.ts — circom compilation, ptau download, Groth16 setup
├── src/writer.ts — file I/O, directory layout
├── src/ceremony.ts — multi-party trusted setup (init / contribute / finalize / verify)
└── src/cli.ts — interactive prompts and orchestration
assets/
├── poseidon-cipher.circom — Poseidon sponge encryption
├── poseidon-constants-old.circom
└── ecdh.circom — Baby Jubjub ECDH
The circuits include circomlib primitives (babyjub.circom, bitify.circom, etc.) which are bundled as a dependency.
This project implements EIP-XXXX: ZK-Encrypted ERC-721 with On-Chain Encrypted Metadata.
All original code © GNU Affero General Public License. Third-party libraries and dependencies retain their original licenses.
