Setup & deployment
Prerequisites
- Node.js ≥ 20 (developed on 24), npm ≥ 10
- A Solana wallet — Phantom, Solflare (extension or web wallet at solflare.com), Backpack or Glow — set to Devnet for local testing
- Circom 2.2.x only if you want to rebuild the circuit — compiled artifacts are committed
Local quick start
npm install
npm run keygen # writes app/.env.local with fresh attester + registry keys
Get a token to prove. Either set GHOST_SNAPSHOT_MINTS in app/.env.local to any SPL mint you hold on devnet, or create a test token:
npm run devnet:mint -- <YOUR_WALLET_ADDRESS> --amount=1500
# prints: GHOST_SNAPSHOT_MINTS=<mint> → paste into app/.env.local
(If the devnet faucet rate-limits the airdrop, fund the printed fee-payer address at https://faucet.solana.com and re-run.)
npm run dev # builds the SDK, starts http://localhost:3000
Then in the browser:
- Create → connect wallet → Sign to unlock → Generate & register (second signature).
- Dashboard → Take snapshot now (dev mode). Your ghost should show its balance, decrypted locally.
- Verify → Ghost Gate → choose a threshold → Request access with GHOST → a popup opens on Prove → Generate zero-knowledge proof → Send to localhost:3000. The gate verifies server-side and shows every check.
- Repeat step 3 in “1 entry / wallet” mode: the nullifier check rejects the second attempt. Try a threshold above your balance: GHOST refuses to prove.
Environment variables (app/.env.local)
| Variable | Required | Description |
|---|---|---|
GHOST_PUBLIC_URL | yes | Public origin. Bound into registry messages and the demo appId. |
NEXT_PUBLIC_SOLANA_CLUSTER | yes | devnet / mainnet-beta. Bound into key derivation (changing it changes every user's wallet key) and attestations. |
SOLANA_RPC_URL | yes | Server-side RPC for snapshots. Use a private node in production. |
NEXT_PUBLIC_SOLANA_RPC_URL | no | Endpoint handed to the wallet adapter. |
GHOST_SNAPSHOT_MINTS | yes | Comma-separated SPL mints to snapshot. |
GHOST_ATTESTER_SECRET_KEY | yes | Base58 64-byte Ed25519 secret key. Secret. |
GHOST_TRUSTED_ATTESTERS | no | Comma-separated attester public keys accepted by this deployment's verifiers. Defaults to the attester's own key. |
GHOST_REGISTRY_ENCRYPTION_KEY | yes | 64 hex chars. Encrypts the registry at rest. Secret. Losing it loses the registry. |
GHOST_ADMIN_TOKEN | prod | Bearer token for POST /api/snapshots. |
GHOST_ALLOW_PUBLIC_SNAPSHOT | no | true lets anyone trigger snapshots (dev). Refused in production. |
GHOST_DATA_DIR | no | Default app/data. |
GHOST_MAX_IDENTITIES_PER_WALLET | no | Default 64. |
Tests
npm test # SDK (41 tests, real Groth16 proofs) + app (11 registry/storage tests)
npm run typecheck
What is covered:
| Requirement | Tests |
|---|---|
| Valid proofs | protocol.test.ts › valid proofs (incl. exact threshold, no address/balance in bundle) |
| Invalid balances | › invalid balances — prover refusal and raw circuit rejection (below threshold, inflated balance, field wrap-around) |
| Forged proofs | › forged proofs — tampered points, altered signals, aliasing, untrusted attester, spliced attestation, wrong mint |
| Expired proofs | › expiry — verifier expiry, prover refusal, stale snapshot |
| Replay attacks | › replay and sybil resistance — reused challenge, foreign challenge, wallet-mode double claim, cross-app reuse |
| Registry security | app/test/registry.test.ts — forged/altered signatures, cross-registry and stale messages, nonce replay, one wallet key per wallet, revocation authz, encryption at rest |
| Live chain | rpc.integration.test.ts (opt-in) — real finalized devnet balance → snapshot → proof → verify |
Production deployment
GHOST runs as a single Node.js server (npm run build && npm start). The bundled JSON-file stores assume one instance with a persistent disk (a VM, container with a volume, Fly.io/Render/Railway with a volume). Serverless platforms with ephemeral filesystems (e.g. Vercel functions) need the stores replaced with a database first: implement ChallengeStore/NullifierStore (SDK interfaces) and swap JsonFileStore in app/src/lib/server/services.ts for Postgres/Redis.
npm ci
npm run build
NODE_ENV=production npm start # port 3000; put TLS + a reverse proxy in front
Schedule snapshots instead of letting users trigger them:
# every 6 hours
0 */6 * * * cd /srv/ghost && GHOST_ADMIN_TOKEN=… GHOST_PUBLIC_URL=https://ghost.example npm run snapshot -w ghost-app
Production checklist
- Independent security audit of circuit, SDK and server. Until then, do not claim production security.
- <a id="trusted-setup"></a>MPC phase-2 trusted setup with several independent contributors and a public beacon; publish the transcript and new vkey hash.
- Attester key in an HSM / KMS; consider multiple attesters run by independent parties.
- Private or self-hosted Solana RPC for snapshots (privacy and integrity).
-
GHOST_ALLOW_PUBLIC_SNAPSHOTunset; strongGHOST_ADMIN_TOKEN; cron snapshots. - Back up
GHOST_REGISTRY_ENCRYPTION_KEYand the data directory separately. - TLS everywhere; reverse proxy that does not log client IPs if you want to reduce metadata.
- Nonce-based CSP (remove
unsafe-inline), HSTS. - Database-backed stores for multi-instance deployments; atomic challenge/nullifier operations.
- Monitor snapshot slot ranges (
slotMax - slotMin) and alert if wide. - Publish attester public keys and artifact hashes out-of-band for verifiers to pin.
Troubleshooting
| Symptom | Cause |
|---|---|
| “this wallet is already bound to a different GHOST wallet key” | You unlocked on a different host or cluster (key derivation is bound to both). Use the original origin. |
| Ghost shows “pending next snapshot” | Registered after the latest snapshot. Take/await a snapshot. |
| “not in snapshot” with tokens in the wallet | Mint not in GHOST_SNAPSHOT_MINTS, or zero finalized balance at snapshot time. |
| Snapshot fails with 429 | Public RPC rate limit; use a dedicated RPC. |
| Popup closes without result | Pop-up blocker, or callbackOrigin mismatch (GHOST refuses to deliver). |