Anyone can see where problems are and whether they're fixed. What stays secret is the community's verdict: every verification vote is encrypted in the browser, tallied homomorphically on-chain (Fhenix CoFHE), and only a single verified / not-verified boolean is ever revealed. The reporter can't game the process by watching the count, and no server ever holds a raw vote.
| Data | Where | Encrypted? |
|---|---|---|
| Vote value (yes=1 / no=0) | on-chain | ✅ euint32, encrypted in the browser |
| Per-complaint tallies | on-chain | ✅ euint32, never decryptable by anyone |
| Verified status | on-chain | derived ebool; only this is ever revealed |
| Voter trust / reputation | on-chain | ✅ euint32, hidden even from the user; only a "trusted?" ebool reaches the authority |
| Complaint risk / urgency score | NLP service (plaintext) | ❌ public intelligence — orders the authority queue |
| Voter / reporter identity | wallets | pseudonymous, never surfaced as PII |
| Photo, category, zone, status, reviews | off-chain + public metadata | ❌ powers the map & analytics |
One-line pitch: the NLP layer figures out which problems look urgent; Fhenix privately decides which reports are trustworthy and which verifiers to trust — computed on encrypted data no one can read or game.
A voter who backs a complaint that the community later verifies earns +1
confidential trust (claimTrust). The award is vote * verified computed in
ciphertext, so it only counts a YES vote — but the raw score is never
FHE.allow'd to anyone, not even the earner. Only a derived trusted? boolean
(trust >= 3) is shared, and only with the authority, which uses it to
notify high-trust verifiers first about new issues in their area. See the
Authority view's "Trusted verifiers — notification routing" panel.
risk_service/ is a small FastAPI app that scores each complaint's urgency
(0..1) from its text + category and flags near-duplicates, ordering the
authority queue highest-risk first. It is deliberately separate from the FHE
layer — public intelligence, no encryption. Optional: if it's not running,
the queue falls back to newest-first and nothing breaks. See
risk_service/README.md.
contracts/— Hardhat +@cofhe/hardhat-plugin.CivicComplaints.sol+ full test suite (including tests that the tallies are undecryptable and that rigged ballots are clamped homomorphically).frontend/— Vite + React + Tailwind + wagmi +@cofhe/sdk(browser-side TFHE encryption). Five views: Feed (encrypted voting), Report, My complaints (private unseal of the verified flag), Authority, Analytics.
# 1. contracts
cd contracts && npm install
npx hardhat test # 7 CivicComplaints + 12 Counter tests
npx hardhat node # terminal A — auto-deploys CoFHE mocks
npx hardhat run scripts/deploy.ts --network localhost # terminal B
npx hardhat run scripts/seed.ts --network localhost # optional demo data
# 2. frontend
cd ../frontend && npm install
npm run dev # http://localhost:5173The deploy script writes VITE_CONTRACT_ADDRESS into frontend/.env.local
automatically. Users connect with MetaMask and are identified only by
their wallet address. The authority is the dedicated wallet
0x953D3747B46001764de006dC638115a480C9Bc76 — import that account into
MetaMask to use the Authority view (override with AUTHORITY=0x... when
running the deploy script). Photos are stored in localStorage unless Supabase
is configured (see below).
For wallet-less local testing only, set VITE_DEV_WALLETS=1 in
frontend/.env.local to get a dropdown of hardhat dev accounts that the node
signs for.
- A citizen files a geotagged pothole photo → complaint registered on-chain.
- Three other wallets each cast an encrypted yes — expand “Show what the chain stores” to display the ciphertext handles nobody can decrypt.
- The reporter → My complaints → Check status (private) → Verified ✓ (“computed from encrypted votes; the tally was never revealed”), then Finalize on-chain.
- The authority wallet resolves it with a fixed-area photo → complaint closed.
- Analytics: per-area, per-category, open vs resolved, timeline.
cd contracts
echo PRIVATE_KEY=0x... > .env # funded with Arbitrum Sepolia ETH
npx hardhat run scripts/deploy.ts --network arb-sepolia
# frontend/.env.local now has the new address; switch MetaMask to Arb SepoliaVoting on testnet uses the real CoFHE coprocessor — the flow is identical, the mocks are simply replaced by the live threshold network.
CivicComplaints is deployed on Arbitrum Sepolia at
0x5F400d9C2A6A97AeDD9Ae69bcB37dc10271e1882
(authority 0x953D3747B46001764de006dC638115a480C9Bc76). Connect MetaMask on
Arbitrum Sepolia and everything — registration, encrypted votes, trust claims,
resolution — is real transactions using Fhenix's live CoFHE coprocessor.
A YES vote requires evidence: the verifier must upload their own geotagged photo of the problem and write a short review. If the complaint has coordinates, the proof photo's EXIF GPS must be within 5 km of the reported spot or the vote is rejected client-side. The vote itself is still encrypted — the proof shows you were there; the ballot stays secret. A NO (dispute) vote needs no proof.
- Create a project at supabase.com.
- Run
supabase/schema.sqlin the SQL editor (tables, open demo policies, realtime on reviews, storage policies). - Create a public storage bucket named
photos. - Set in
frontend/.env.local:
VITE_SUPABASE_URL=https://<project>.supabase.co
VITE_SUPABASE_ANON_KEY=<anon key>
Photos, geo metadata, risk scores, and reviews (with proof photos) then live in Supabase, and new reviews stream to all connected clients in real time. Without these env vars the app falls back to per-browser localStorage.
- Votes are clamped to {0,1} with
FHE.min— a “999 yes” ballot counts as 1. FHE.allowis never called onyesVotes/totalVotes; the ACL makes them permanently undecryptable (covered by tests).verified = FHE.gte(yesVotes, 3)— recomputed per vote; the reporter gets private view access to this boolean only.- The public status flip uses the threshold-network decrypt flow
(
allowPublic → decryptForTx → publishDecryptResult), so “verified” is provable on-chain without trusting the caller.