secret ballot
In secret mode, each ballot is an OC Lock envelope addressed to a poll-specific
X25519 reveal keypair. At deadline the creator publishes reveal_sk. Anyone can
then decrypt all ballots and tally.
how it works
-
Poll creation. Creator generates a fresh X25519 keypair.
reveal_pkgoes into the poll;reveal_skstays local (IndexedDB). -
Ballot. Voter encrypts the chosen option id with OC Lock v2 envelope format to a synthetic recipient whose
device_pk = reveal_pk.ballot.optionis set tonull; the real choice is insideballot.secret.envelope. Theballot.secret.commitbinds the ballot id to the hashed plaintext:commit = sha256("oc-vote/v0/commit\npoll_id: …\nvoter: …\noption: …\n") -
Cast. Voter signs the ballot with BIP-322 and publishes to Nostr kind 30081. Observers see who voted. Nobody (except the creator, who holds
reveal_sk) sees what. -
Deadline. Creator publishes a kind-30082 reveal event:
{ poll_id, reveal_sk, revealed_at, sig }. -
Tally. Every observer verifies the reveal's BIP-322 signature, unseals each ballot, verifies each
commit, and computes the tally. Any commit mismatch drops the ballot (E_COMMIT_MISMATCH).
honest trade-offs
The creator holds reveal_sk in v0. This is named and documented, not hidden.
Early peeking. The creator can decrypt ballots before deadline with their local key. They cannot publish a tally without revealing the key, which is an observable action. A malicious creator could privately know the running score and strategize off of it. Full coercion-resistance requires threshold reveal (future work) or homomorphic tally (Helios-style, out of scope).
Non-reveal. If the creator disappears or refuses to publish the reveal event, the poll is permanently abandoned. Observers can enumerate the sealed ballots — proving participation — but cannot produce a tally. This is a real cost. Communities that can't tolerate it should use public polls or wait for threshold-reveal.
Receipt-freeness. After reveal, every voter's choice is publicly linkable to their Bitcoin address. A coercer can demand proof of vote. This is a failure of receipt-freeness that v0 does not attempt to fix.
future: threshold reveal
v1 will offer two alternatives to creator-held reveal:
- n-of-m trustees.
reveal_skis secret-shared acrossmparties; anyncan reconstruct. Names each trustee publicly. - drand tlock.
reveal_skis locked to a future drand beacon round. No party holds it until the round fires.
Both eliminate the "single creator can peek early" and "single creator can refuse to reveal" failure modes. Both add coordination overhead and are why v0 ships the simpler version first.
status in this client
Secret mode is fully wired end-to-end as of this release:
/create— pickballot mode: secretto generate a local X25519 reveal keypair.reveal_pkis embedded in the poll;reveal_skis saved to this browser's IndexedDB./p/<poll_id>— for secret-mode polls, the ballot form seals your chosen option id into an OC Lock v2 envelope addressed toreveal_pkand signs the outer ballot with BIP-322. Live tally showsawaiting revealuntil the creator publishes./reveal/<poll_id>— the creator's ceremony page. After deadline, paste (or load from local storage)reveal_sk, sign a BIP-322 over the reveal object's id, publish kind 30082.
Envelope composition uses
@orangecheck/lock-core —
the same primitive that powers lock.ochk.io. OC Vote
doesn't ship its own crypto; it composes.