nostr-anon-vote
v0.1.0
Published
Anonymous voting on Nostr with LSAG ring signatures — double-vote prevention without revealing identity
Maintainers
Readme
nostr-anon-vote
Anonymous voting on Nostr with LSAG ring signatures — double-vote prevention without revealing identity.
Features
- Anonymous ballots — LSAG ring signatures prove group membership without revealing which member voted
- Double-vote prevention — Key images detect duplicate votes without unmasking the voter
- Re-vote support — Optional policy allowing voters to change their mind (last ballot counts)
- Encrypted ballots — Vote content encrypted until tallying
- Identity-agnostic — Works with any ring of Nostr pubkeys, not tied to any specific identity system
- Nostr-native — Builds standard Nostr events (kinds 30482–30484)
Install
npm install nostr-anon-voteQuick Start
import { createElection, castBallot, tallyElection } from 'nostr-anon-vote';
// 1. Create an election
const election = await createElection(organizerPrivkey, {
title: 'Should we adopt proposal X?',
options: ['Yes', 'No', 'Abstain'],
eligiblePubkeys: [pubkey1, pubkey2, pubkey3],
closesAt: Math.floor(Date.now() / 1000) + 86400, // 24h
scale: 'community',
reVotePolicy: 'allowed',
});
// 2. Cast a ballot
const ballot = await castBallot(voterPrivkey, {
electionEventId: election.id,
vote: 'Yes',
ring: [pubkey1, pubkey2, pubkey3],
signerIndex: 0, // voter's position in the ring
});
// 3. Tally results
const result = await tallyElection(organizerPrivkey, {
electionEvent: election,
ballotEvents: [ballot],
tallyKey: tallyPrivkey,
});Event Kinds
| Kind | Name | Purpose | |------|------|---------| | 30482 | Election | Define an election with eligible voters and options | | 30483 | Ballot | Anonymous signed ballot with LSAG key image | | 30484 | Election Result | Tallied result with verification data |
Kind numbers are placeholders pending NIP assignment.
How It Works
- An organiser publishes an Election event listing eligible pubkeys and voting options
- Each voter constructs an LSAG ring signature over their encrypted vote, using the eligible pubkeys as the ring
- The ring signature proves "I am one of the eligible voters" without revealing which one
- A key image is included — unique per voter per election, enabling double-vote detection
- After the election closes, a tally authority decrypts and counts valid ballots
- The Election Result is published with verification data so anyone can audit
Cryptography
- LSAG (Linkable Spontaneous Anonymous Group) signatures on secp256k1 via
@forgesworn/ring-sig - ECDH + HKDF for ballot encryption (voter ↔ tally authority)
- Schnorr signatures (BIP-340) for Nostr event signing
API
Election
createElection(privkey, params)— Create and sign an election eventparseElection(event)— Parse an election event into structured datavalidateElection(event)— Validate election event structure
Voting
castBallot(privkey, params)— Cast an anonymous ballot with LSAG signatureverifyBallot(ballot, election)— Verify ballot signature and eligibilityvalidateBallot(event)— Validate ballot event structure
Tallying
tallyElection(privkey, params)— Decrypt and count ballots, publish resultencryptBallotContent(plaintext, sharedSecret)— Encrypt vote contentdecryptBallotContent(ciphertext, sharedSecret)— Decrypt vote contentvalidateElectionResult(event)— Validate result event structure
Part of the ForgeSworn Toolkit
ForgeSworn builds open-source cryptographic identity, payments, and coordination tools for Nostr.
| Library | What it does | |---------|-------------| | nsec-tree | Deterministic sub-identity derivation | | ring-sig | SAG/LSAG ring signatures on secp256k1 | | range-proof | Pedersen commitment range proofs | | canary-kit | Coercion-resistant spoken verification | | spoken-token | Human-speakable verification tokens | | toll-booth | L402 payment middleware | | geohash-kit | Geohash toolkit with polygon coverage | | nostr-attestations | NIP-VA verifiable attestations | | dominion | Epoch-based encrypted access control | | nostr-veil | Privacy-preserving Web of Trust |
Licence
MIT
