Skip to content

stanbar/stellot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stellot† on Soroban

A cryptographically real proof-of-concept of the Stellot† e-voting protocol (PhD thesis ch. 04a / 04b / 05a) built on Stellar Soroban.


What is Stellot†?

Stellot† is a receipt-free, coercion-resistant e-voting protocol based on:

  • Threshold ElGamal encryption on secp256k1 — votes are encrypted under a combined key; no single party can decrypt alone
  • Feldman VSS / DKG — key-holders collectively generate the election key without any trusted dealer
  • Shamir secret sharing + Lagrange interpolation — any t-of-m key-holders can perform the decryption without reconstructing the private key
  • Hash-based nullifiers — prevent double-voting and double-issuance
  • Soroban smart contract — enforces the protocol rules on-chain

Directory Structure

stellot-dagger/
├── Cargo.toml                        # Rust workspace
├── contracts/
│   └── election/
│       ├── Cargo.toml
│       └── src/
│           ├── lib.rs                # contract entry points (deploy, issue, cast, tally)
│           ├── types.rs              # DataKey, ElectionParams, EncryptedBallot
│           ├── merkle.rs             # SHA-256 Merkle verification
│           ├── error.rs              # ContractError enum
│           └── test.rs               # 7 unit tests
├── web/                              # Next.js 15 App Router frontend
│   └── src/
│       ├── app/                      # pages
│       └── lib/
│           ├── crypto.ts             # ElGamal, nullifiers, Lagrange, message hashing
│           ├── merkle.ts             # Merkle tree build + prove
│           ├── dkg.ts                # Feldman VSS DKG
│           ├── threshold.ts          # Shamir + Lagrange interpolation
│           ├── contract.ts           # Soroban RPC wrappers
│           └── wallet.ts             # Freighter + in-memory keypair
├── scripts/
│   ├── dkg.ts                        # CLI: run DKG ceremony
│   ├── distributor.ts                # CLI: distributor service (sign issuances)
│   ├── post_share.ts                 # CLI: KH posts decryption shares + tally
│   └── e2e.sh                        # full runbook (local sandbox)
└── keys/                             # generated by scripts (gitignored)

Real vs. Mocked Components

Component Implementation Status
ElGamal encryption Full exponential ElGamal on secp256k1 (@noble/curves) Real
DKG / VSS Feldman VSS ceremony with real algebra, simulated multi-party Real algebra
KH threshold decryption Shamir + Lagrange interpolation on EC partial decryptions Real
Nullifiers SHA-256(domain ‖ sk ‖ eid) Real
Cast proof Ed25519 signature from casting account Real
Distributor multi-sig M-of-N Ed25519 signatures verified on-chain Real
Eligibility proof SHA-256 Merkle inclusion proof (non-ZK) Simplified
CP proofs for shares Generated off-chain, logged; on-chain only checks KH sig Partial §

‡ Merkle vs. ZK: The current eligibility proof is a Merkle inclusion proof, which reveals the leaf index (position in the voter list). In the full Stellot† protocol this is replaced by a Groth16 SNARK that proves membership without revealing position. The Merkle approach is a direct simplification with identical on-chain verification cost (~20 SHA-256 calls for 1M voters).

§ CP proofs: Chaum-Pedersen proofs for partial decryptions (proving D_j = C1^sk_j correctly) are computed off-chain in scripts/post_share.ts and can be verified by any observer. On-chain, the contract only verifies that each submitter is in the KH roster and holds the correct Ed25519 identity key. Full on-chain CP verification would require raw scalar-mul host functions, which are not yet stable in Soroban.


Protocol Stages

Stage 0 — Deploy

  1. Organizer runs scripts/dkg.ts --m 3 --t 2 → Feldman VSS generates a combined secp256k1 public key and per-KH Shamir shares
  2. Organizer builds a SHA-256 Merkle tree of eligible voter pubkeys
  3. deploy() stores all parameters on-chain

Stage 1 — Issue (Casting Account Registration)

  1. Voter generates a fresh Ed25519 casting keypair in the browser
  2. Voter computes nf_issue = SHA-256("stellot:issue" ‖ voter_sk ‖ eid)
  3. Voter sends (pk_cast, nf_issue) to distributors
  4. Distributor verifies eligibility and signs SHA-256("stellot:issue" ‖ eid ‖ pk_cast ‖ nf_issue)
  5. Once dist_threshold signatures collected, issue_account() is called on-chain

Stage 2 — Cast

  1. Voter selects option v and encrypts: C1 = r·G, C2 = (v+1)·G + r·PK
  2. Voter computes nf_cast = SHA-256("stellot:cast" ‖ sk_cast ‖ eid)
  3. Voter signs the ballot with their casting key (Ed25519)
  4. cast() verifies: window ✓, nullifier fresh ✓, account registered ✓, sig valid ✓

Stage 3 — Tally

  1. After end_time, each KH computes D_ji = C1_i^sk_j (partial decryption per ballot)
  2. KH signs the batch and calls post_share()
  3. Once kh_threshold shares posted: finalize_tally() accepted
  4. Browser / CLI combines shares via Lagrange → D_i = Σ(λ_j · D_ji)
  5. Recovers V = C2 - D_i = (v+1)·G, finds v by brute-force DL search

Quick Start

1. Run Contract Tests

cargo test
# Expected: 7 passed, 0 failed

2. Build WASM

# Install the correct Soroban WASM target first (once):
rustup target add wasm32v1-none

# Build (uses wasm32v1-none — no reference-types, required by the Soroban VM):
stellar contract build
# Output: target/wasm32v1-none/release/election.wasm

3. Run DKG Ceremony

npx tsx scripts/dkg.ts --m 3 --t 2 --output ./keys/

4. End-to-End (local sandbox)

# Requires stellar-cli and Node.js >= 18
bash scripts/e2e.sh
# The contract id is printed at the end:
#   Contract: C...

5. Web App

The contract ID is printed by bash scripts/e2e.sh (step 4) at the end of its output (Contract: C...). To deploy a contract manually without the full e2e runbook, use:

stellar contract build
stellar contract deploy \
  --wasm target/wasm32v1-none/release/election.wasm \
  --source <your-stellar-key-name> \
  --network local        # or testnet / mainnet
# Prints: C<contract-id>

Then start the web app pointing at that contract:

cd web
npm install
export NEXT_PUBLIC_CONTRACT_ID=C<contract-id-from-above>
export NEXT_PUBLIC_RPC_URL=http://localhost:8000/soroban/rpc   # local sandbox
# export NEXT_PUBLIC_RPC_URL=https://soroban-testnet.stellar.org  # testnet
export NEXT_PUBLIC_NETWORK_PASSPHRASE="Standalone Network ; February 2017"  # local
# export NEXT_PUBLIC_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"  # testnet
npm run dev

Cryptographic Primitives

Exponential ElGamal on secp256k1

keygen():   sk ∈ Z_q random,  PK = sk · G
encrypt(v): r ∈ Z_q random,   C = (r·G, (v+1)·G + r·PK)
decrypt(C): M = C2 - sk·C1 = (v+1)·G,  brute-force v

SHA-256 Merkle Tree

leaf(x):      SHA256("stellot:leaf" ‖ x)
node(l, r):   SHA256("stellot:node" ‖ l ‖ r)

Domain separation prevents second-preimage attacks.

Feldman VSS

Party j samples polynomial f_j(x) = a_j0 + a_j1·x + … + a_j(t-1)·x^(t-1) over Z_q, publishes commitments A_jk = a_jk · G, and sends share s_ji = f_j(i) to party i. Verification: s_ji · G == Σ_k(A_jk · i^k). Combined key: PK = Σ_j(A_j0).

Lagrange Interpolation

λ_j = ∏_{k≠j}(k / (k-j))  mod q
D_i = Σ_j(λ_j · D_ji)

Nullifiers

nf_issue = SHA256("stellot:issue" ‖ sk_voter ‖ eid_le64)
nf_cast  = SHA256("stellot:cast"  ‖ sk_cast  ‖ eid_le64)

Contract Storage Layout

Key Value
NextElectionId u64 — next eid
Election(eid) ElectionParams
EligibleRoot(eid) BytesN<32> — Merkle root
DistRoster(eid) Vec<BytesN<32>> — distributor Ed25519 pubkeys
KhRoster(eid) Vec<BytesN<32>> — KH Ed25519 pubkeys
KhCommitment(eid, idx) Bytes — 33-byte VSS commitment A_j0
IssueNullifier(eid, nf) bool
CastingAccount(eid, pk) bool
CastNullifier(eid, nf) bool
Ballot(eid, idx) EncryptedBallot { nf_cast, c1, c2 }
KhShare(eid, idx) Bytes — serialised share batch
Tally(eid) Vec<u32>

Dependencies

Rust

Crate Version Purpose
soroban-sdk 22.0.0 Soroban smart contract SDK
ed25519-dalek 2 Ed25519 signing (tests only)

TypeScript

Package Version Purpose
@noble/curves ^1.6.0 secp256k1 + Ed25519
@noble/hashes ^1.5.0 SHA-256
@stellar/stellar-sdk ^13.0.0 Soroban RPC
next ^15.0.0 Frontend framework

Future Work (per thesis roadmap)

  1. ZK eligibility proofs — replace Merkle with Groth16 SNARK for anonymity
  2. On-chain CP verification — once Soroban exposes raw EC scalar mul
  3. Full FDKG — federated DKG replacing the simulated ceremony
  4. ZK cast proof — replace Ed25519 sig with proof linking nullifier to voter secret
  5. Coercion resistance — receipt-freeness via re-encryption mixnets or homomorphic tallying

About

i-voting platform powered by Stellar blockchain

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors