MayurK-cmd/GhostList

Rise In x Midnight Challenge

★ 0Forks 0TypeScriptGitHub ↗Compare

Project website ↗

README

Ghostlist

CI License: MIT Node Midnight

Prove you belong on the list — without ever revealing the list or which entry is yours.

Ghostlist is a privacy-preserving allowlist mint gate built on the Midnight Network. It uses zero-knowledge proofs so that allowlist members can mint a token without exposing their wallet address, the index of their entry, or any other identifying information.


Live Demo

→ ghostlist-rho.vercel.app

Connect Lace, click Mint Ghost, and the entire proof generation + transaction submission happens in your browser. Your secret never leaves your device.


Deployed Contract

Network Contract Address
Preprod 0x64261388057269dee716e43acec16fb20d55a804ce1865b77922cb0513db4e6e

View on Midnight Explorer (Preprod) or query the contract address in any Midnight indexer that supports Preprod.


The Problem

Every web3 drop starts with an allowlist — a list of wallets eligible to mint early. Publishing that list on a public blockchain means every participant is doxxed before the drop even begins. Phishers scrape the list and swarm each wallet with DMs, fake mint sites, and targeted scams.

Ghostlist solves this. The contract stores only a cryptographic commitment to the allowlist (a Merkle root). When a user mints, their browser generates a zero-knowledge proof that they are on the list — without revealing which entry is theirs or any identifying information.


Highlights

  • Real Lace wallet integration — no mocks, no placeholder wallet
  • End-to-end ZK pipeline — proof generated in browser, verified on-chain
  • 50-wallet cohort test — validated at scale on Preprod
  • Production frontend — TanStack Start (React 19 + Vite 8 + Nitro SSR), deployed on Vercel
  • Full test coverage — 4/4 passing on the core contract, plus e2e and cohort suites
  • CI/CD — GitHub Actions pipeline on every push and PR

Privacy Model

What an on-chain observer CAN see

  • merkleRoot — the Merkle root committing to the full allowlist
  • usedNullifiers — which nullifiers have been consumed (prevents double-minting)
  • totalMinted — public counter of successful mints

What an on-chain observer CANNOT learn

  • Which allowlist entry minted — nullifier is a one-way hash
  • The user's secret — witness is private to the user's device
  • The allowlist itself — only the root is on-chain
  • Whether two mints came from the same user — nullifiers are unlinkable
  • The user's wallet address — not correlated with the nullifier

This is the property that makes allowlists safe to publish.


Tech Stack

Layer Technology
ZK contract Compact (@midnight-ntwrk/compact-js v2.5)
Runtime Midnight.js SDK v4.1, Compact.js runtime v0.16
Wallet Lace Midnight (real dApp connector)
Frontend TanStack Start + React 19 + Vite 8 + Nitro SSR
Routing TanStack Router v1.170
Styling Tailwind CSS v4 + shadcn/ui primitives
Animation GSAP 3.15 + Anime.js 4.5
Data TanStack Query v5
Proof server Docker (midnightnetwork/proof-server)
Indexer Midnight public indexer (Preprod)
State Midnight level private state provider
CI GitHub Actions (Node 22)
Deploy Vercel (frontend)

Repository Structure

ghostlist/
├── contracts/
│   ├── allowlist_stub.compact       # The ZK contract (single file)
│   └── managed/                     # Compiled circuits + keys
├── managed/                         # Mirror for the frontend
├── src/
│   ├── deploy.ts                    # Contract deployment
│   ├── cli.ts                       # CLI mint tool
│   ├── setup.ts                     # One-shot onboarding
│   └── wallet.ts                    # Wallet creation + key derivation
├── scripts/
│   ├── precompute-tree.ts           # Multi-entry Merkle tree generator
│   ├── derive-address.ts            # Address derivation from seed
│   └── cohort/                      # 50-wallet integration tooling
├── tests/
│   └── allowlist_stub.test.ts       # Core test suite (4/4 passing)
├── frontend/
│   ├── src/hooks/
│   │   ├── useMidnight.ts           # WalletProvider (real Lace integration)
│   │   ├── useMint.ts               # Mint hook (proof server + wallet submit)
│   │   └── useWallet.ts             # Wallet state wrapper
│   ├── src/routes/
│   │   ├── index.tsx                # Landing page
│   │   ├── mint.tsx                 # Mint page UI
│   │   └── faq.tsx                  # Privacy FAQ
│   ├── src/components/site/
│   │   ├── GhostCard.tsx            # Animated mint card
│   │   ├── ProofPanel.tsx           # ZK proof preview (user feedback)
│   │   └── ...                      # Navbar, Hero, Footer, etc.
│   ├── src/lib/contract/            # Browser Midnight.js provider stack
│   └── public/                      # Static ZK artifacts + compiled contract + tree
├── docs/
│   ├── SETUP.md                     # Local development setup (in depth)
│   ├── USAGE.md                     # End-user walkthrough
│   ├── COHORT_TESTING.md            # 50-wallet integration testing
│   └── FEEDBACK.md                  # User feedback & iteration log
├── .github/workflows/
│   └── ci.yml                       # CI/CD pipeline
├── docker-compose.yml               # Local proof server
├── PROPOSAL.md                      # Product proposal (Level 3)
└── README.md                        # You are here

Quick Start

git clone https://github.com/MayurK-cmd/GhostList.git
cd GhostList
npm install
cd frontend && npm install && cd ..

# Start the proof server
docker compose up -d

# Generate a fresh Merkle tree
npx tsx scripts/precompute-tree.ts > frontend/public/tree.json

# Deploy to Preprod (requires MIDNIGHT_WALLET_SEED)
export MIDNIGHT_WALLET_SEED=your_64_char_hex_seed
npx tsx src/deploy.ts --network preprod

# Launch the frontend
cd frontend && npm run dev

Open http://localhost:8080, connect Lace, and mint.

For the complete local setup walkthrough — environment variables, Compact compilation, troubleshooting, and CI parity — see docs/SETUP.md.


Prerequisites

  • Lace Midnight wallet — install from GitHub, funded on Preprod
  • Node.js v22 or later
  • Docker Desktop (for the local proof server)
  • Compact compiler (optional — managed artifacts are committed)

How a Mint Works

┌──────────────┐    1. Generate witness     ┌──────────────────────┐
│   Browser    │ ─────────────────────────► │ Local: precompute    │
│  (Lace + ZK) │                            │ Merkle path + secret │
└──────┬───────┘                            └──────────┬───────────┘
       │                                              │
       │ 2. Request proof (HTTP)                      │
       ▼                                              ▼
┌──────────────┐                            ┌──────────────────────┐
│ Proof Server │ ◄────── ZK circuit ────────│ contracts/managed/   │
│   (Docker)   │       inputs + witness     │ (proving key)        │
└──────┬───────┘                            └──────────────────────┘
       │
       │ 3. Return ZK proof
       ▼
┌──────────────┐    4. Sign + submit         ┌──────────────────────┐
│   Lace       │ ─────────────────────────► │ Midnight Preprod     │
│  (wallet)    │    tx with proof attached  │ contract verifies +  │
└──────────────┘                            │ nullifier consumed   │
                                            └──────────────────────┘

The proof attests:

  1. I know a secret that hashes to one of the leaves of the committed Merkle tree.
  2. I have not used this nullifier before.

Nothing else is revealed.


Run Tests

npm test

Covers:

  1. A valid allowlist member can mint successfully
  2. A non-member (invalid Merkle path) is rejected
  3. The same secret cannot mint twice (nullifier reuse blocked)
  4. The private secret and Merkle path never appear in test output

End-to-end and cohort suites:

npm run test:e2e
npm run test:cohort

CI/CD

The project uses GitHub Actions (.github/workflows/ci.yml):

  • Triggers: push to main / master, plus every pull request
  • Steps: checkout, install Node 22, install root + frontend deps, optional Compact compile via Docker (best-effort), full test suite
  • Badge: the green CI badge at the top of this README reflects the most recent run on master

User Feedback & Iteration

Frontend improvements based on real user feedback (anonymized):

  • "Explain zero-knowledge in simpler terms" → Hero section now explains privacy in plain English
  • "I want to see the proof before submitting" → New ProofPanel component shows ZK proof details before submission
  • "Show me the transaction on-chain" → Post-mint, display Midnight Explorer link with transaction hash

See docs/FEEDBACK.md for the full feedback loop and all changes implemented.


Documentation


Demo Video

See /demo-vid/ for the demo video showing full MVP functionality.


Security Notes

  • The MIDNIGHT_WALLET_SEED in .env is never committed — .gitignore excludes it
  • Production deploys use a freshly funded throwaway seed
  • The proof server runs locally in Docker and never persists witnesses
  • ZK artifacts served from frontend/public/ are content-hashed by the build pipeline

If you find a security issue, please open a private issue or contact the maintainers directly — do not disclose in public issues.


Contributing

We welcome PRs. See CONTRIBUTING.md for the workflow, branch naming, and commit conventions. For substantial changes, please open an issue first to discuss the design.


License

MIT — see the license file for details.

Built for the Midnight Builder Challenge

Contributors

MayurK-cmd

Issues