CA
GHOST

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:

  1. Create → connect wallet → Sign to unlock → Generate & register (second signature).
  2. Dashboard → Take snapshot now (dev mode). Your ghost should show its balance, decrypted locally.
  3. 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.
  4. 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)

VariableRequiredDescription
GHOST_PUBLIC_URLyesPublic origin. Bound into registry messages and the demo appId.
NEXT_PUBLIC_SOLANA_CLUSTERyesdevnet / mainnet-beta. Bound into key derivation (changing it changes every user's wallet key) and attestations.
SOLANA_RPC_URLyesServer-side RPC for snapshots. Use a private node in production.
NEXT_PUBLIC_SOLANA_RPC_URLnoEndpoint handed to the wallet adapter.
GHOST_SNAPSHOT_MINTSyesComma-separated SPL mints to snapshot.
GHOST_ATTESTER_SECRET_KEYyesBase58 64-byte Ed25519 secret key. Secret.
GHOST_TRUSTED_ATTESTERSnoComma-separated attester public keys accepted by this deployment's verifiers. Defaults to the attester's own key.
GHOST_REGISTRY_ENCRYPTION_KEYyes64 hex chars. Encrypts the registry at rest. Secret. Losing it loses the registry.
GHOST_ADMIN_TOKENprodBearer token for POST /api/snapshots.
GHOST_ALLOW_PUBLIC_SNAPSHOTnotrue lets anyone trigger snapshots (dev). Refused in production.
GHOST_DATA_DIRnoDefault app/data.
GHOST_MAX_IDENTITIES_PER_WALLETnoDefault 64.

Tests

npm test                 # SDK (41 tests, real Groth16 proofs) + app (11 registry/storage tests)
npm run typecheck

What is covered:

RequirementTests
Valid proofsprotocol.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 securityapp/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 chainrpc.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_SNAPSHOT unset; strong GHOST_ADMIN_TOKEN; cron snapshots.
  • Back up GHOST_REGISTRY_ENCRYPTION_KEY and 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

SymptomCause
“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 walletMint not in GHOST_SNAPSHOT_MINTS, or zero finalized balance at snapshot time.
Snapshot fails with 429Public RPC rate limit; use a dedicated RPC.
Popup closes without resultPop-up blocker, or callbackOrigin mismatch (GHOST refuses to deliver).