Skip to content
Lihim
Open app

Anonymous polls · verified on Stellar

Say what you really think.

Only members can answer. Each member answers one time. Nobody can see who gave which answer. This includes the admin. Anyone can count the answers again.

Prove you belong. Never say who you are.

to prove, in your browser
≈ 1 s
names or emails on-chain
0
answer per member per poll
1
members before a poll opens
5+

The problem

Other group polls fail in one way or another.

A comparison of Lihim with anonymous forms and forms that need a login
What you needAnonymous formLogin formLihim
Only members can answerNoYesYes
One answer per personNoYesYes
Nobody sees who gave which answerYesNoYes
Anyone can count the answers againNoNoYes
Outside people cannot add answersNoYesYes

How it works

Five steps. You see only one touch.

  1. 01

    Join with a passkey

    Open the invite. Use Face ID or your fingerprint. You do not need a password, a wallet or a seed phrase.

  2. 02

    Get a secret for each group

    Your phone derives a new identity for this group. Only its public commitment goes into the tree.

  3. 03

    Prove, do not tell

    When you answer, your browser builds a zero-knowledge proof that you are a member. This takes about one second.

  4. 04

    A relay sends it

    A relay sends the answer for you. The relay uses no cookies and writes no IP addresses to its logs. No Stellar account is connected to you.

  5. 05

    Anyone can count again

    Stellar checks the proof and keeps the nullifier. The public verify page counts the answers again from chain data.

The zero-knowledge part

Semaphore V4, running on Soroban.

Lihim uses Semaphore V4, the anonymous-signalling protocol from PSE. Each answer comes with a Groth16 proof over the BN254 curve. The proof says: “I know the secret of one commitment in the Merkle tree of this group. This is my one-time nullifier for this poll.” The proof does not say which commitment. Try it below.

Only you see the highlighted leaf.
38f8…ab44…6ed9…fa8b…3132…a8ea…you2a6e…9eec…root

Select an answer. Then prove that you are a member.

Public signals

These go with the proof. Anyone can read them on Stellar.

merkleTreeRoot
hidden
nullifier
hidden
hash(message)
hidden
hash(scope)
hidden
π: 8 BN254 field elements
0xda5…38c0x720…38d0x545…5da0x096…12d0x459…2330xad3…37f0x934…a4c0x8ff…473

Private inputs

These stay on your phone. Lihim never sends them and never keeps them.

secretScalar
0xcf6a7a…c8cf
leaf index
5
siblings
3 hashes (dashed in the tree)

Illustrative values: the hashes on this page are stand-ins, not real Poseidon output.

The algorithm, step by step

Lihim uses the same circuits, verification keys and JS libraries as upstream Semaphore V4. Lihim adds the Stellar part: Poseidon and the LeanIMT in a Soroban contract, a Groth16 verifier on the native BN254 host functions of Stellar, and storage for nullifiers.

  1. 01

    Identity, one per group

    prf  = WebAuthn-PRF(passkey, sha256("lihim/v1"))
    sk   = HKDF-SHA256(prf, "lihim/sem/" ‖ group)
    (pk, secretScalar) = EdDSA-Poseidon(sk)  // Baby Jubjub

    Your phone derives it from your passkey. Thus, a new phone gets the same identities. Each group gives a different key. Nobody can connect your groups on-chain.

  2. 02

    Commitment

    C = Poseidon(pk.x, pk.y)

    The public value that goes on-chain. It shows that a person joined. It does not show who.

  3. 03

    Group tree

    root = LeanIMT_Poseidon(C₀, C₁, …, Cₙ₋₁)
    depth ∈ 1..32,  insert ≈ depth hashes

    A Lean Incremental Merkle Tree in the Soroban contract, with Poseidon on-chain. When a poll opens, the contract freezes its root.

  4. 04

    Nullifier

    N = Poseidon(scope, secretScalar)
    scope = poll id,  message = your answer

    The same member and the same poll always give the same N. Thus, the contract rejects a second answer. A new poll gives a different N.

  5. 05

    Groth16 proof over BN254

    public  = [root, N, h(message), h(scope)]
    private = [secretScalar, leaf index, siblings]
    h(x)    = keccak256(x) >> 8
    π       = 8 field elements

    Your browser builds it in a Web Worker. It uses the official Semaphore V4 circuits and the keys from the PSE trusted setup ceremony. There is one verification key for each tree depth.

  6. 06

    On-chain check

    L = IC₀ + Σ sᵢ·ICᵢ            // bn254 g1_msm
    e(−A, B)·e(α, β)·e(L, γ)·e(C, δ) = 1  // pairing_check
    root ∈ recent roots,  N ∉ used  →  store N

    LihimVerifier does the pairing check on the native BN254 host functions of Stellar. It rejects inputs that are not on the curve or not in range. Then LihimSemaphore keeps N and emits ProofValidated.

What the proof guarantees

  • The answer came from a member in the member tree of the group.
  • Each member answers each poll one time (one nullifier for each poll).
  • Nobody can connect your answers in different polls and groups.
  • Each counted answer passed the same on-chain check. Thus, anyone can count the answers again.

What the proof does not do

  • Decide who is a member. The group admin decides this.
  • Hide that you joined. The admin sees the member list. The admin never sees the answers.
  • Make a group of 3 people anonymous. For this reason, each group has a minimum size before a poll can open.
  • Hide the live count from the chain. Lihim hides live results only in the app.

Built with

The technology.

Zero-knowledge

Semaphore V4. Each byte agrees with the official libraries.

  • Semaphore V4
  • Groth16
  • BN254
  • Poseidon
  • LeanIMT
  • Baby Jubjub
  • EdDSA-Poseidon
  • keccak256
  • Circom

Stellar contracts

Three Soroban contracts in Rust: the verifier, the Semaphore groups and the polls.

  • Stellar
  • Soroban
  • Rust
  • soroban-sdk 28
  • soroban-poseidon
  • BN254 host functions

Keys and identity

One passkey signs you in and derives all group identities. You do not write anything down.

  • WebAuthn passkeys
  • PRF extension
  • HKDF-SHA256
  • Ed25519 admin keys
  • @noble/curves

Client and SDK

A PWA with the prover in a Web Worker. Other Stellar apps can use the SDK.

  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind CSS 4
  • Radix UI
  • @lihim/sdk
  • @semaphore-protocol/proof
  • @stellar/stellar-sdk

Answer relay

It pays the fees and sends the proofs. It uses no cookies and no auth header, and it does not log IP addresses.

  • Hono
  • Node.js
  • simulate-then-submit

Off-chain data

Names and invites stay here, behind row-level security. Answers never stay here.

  • Supabase
  • Postgres + RLS
  • Edge Functions (Deno)
  • pg_cron indexer

Rules

Lihim will never do these things.

  • Move money

    No payments, no prizes, no bets and no paid answers. Never.

  • Put your name on-chain

    On-chain, there are only roots, commitments, nullifiers, poll ids and answers. Names and emails stay off-chain. Only your group admin can see them.

  • Promise “100% anonymous”

    We tell you exactly who can see what. In a small group, people can guess an answer. For this reason, each group has a minimum size.

Your group knows the answer. Let the members say it.

Create a group. Share one invite link. Ask the question that nobody asks aloud.