Skip to content

tindzk/dotizen

Repository files navigation

Dotizen

Participatory governance for homeowners. Anonymous, verifiable on-chain voting via Groth16 zero-knowledge proofs.

Built on the Polkadot SDK as a Cumulus-based parachain. Features include coercion-resistant voting, passkey sign-in with multi-device pairing, encrypted on-chain PII, declining quorum, committee elections with public Q&A, IPFS-pinned proposals, and an immutable audit trail.

What's inside

  • Blockchain (blockchain/)
  • Frontend (web/): React + TypeScript app using PAPI for chain interactions and WebAuthn passkeys for sign-in
  • CLI (cli/): Rust CLI for membership and governance operations via subxt
  • Spec (spec/): Domain model, pallet specs, cryptographic design
  • Dev scripts (scripts/): One-command scripts to build, start, test, and deploy

Screenshots

Sign-in landing page
Sign in / register / recover
Dashboard with active proposals
Dashboard
Proposal detail with vote and comments
Proposal detail
My home: devices and voting key
My home

Quick start

Prerequisites

  • Rust (stable, installed via rustup)

  • Node.js 22.x LTS and npm v10.9+

  • OpenSSL development headers (libssl-dev on Ubuntu, openssl on macOS)

  • protoc Protocol Buffers compiler (protobuf-compiler on Ubuntu, protobuf on macOS)

  • zombienet v1.3.x (npm install -g @zombienet/cli)

  • chain-spec-builder v17.0.0 (cargo install staging-chain-spec-builder)

  • Polkadot SDK binaries (stable2512-3):

    # Download pre-built binaries (default)
    ./scripts/download-sdk-binaries.sh
    
    # Or build relay node with fast-runtime for faster finality (recommended for dev)
    ./scripts/download-sdk-binaries.sh --fast-runtime

    The --fast-runtime flag builds the polkadot relay binary from source with reduced epoch duration, making GRANDPA finality take seconds instead of minutes on local Zombienet. First build takes ~15 minutes; subsequent builds are cached.

Environment variables

Copy the example env file and fill in the values you need:

cp web/.env.example web/.env

See web/.env.example for all available variables. At minimum you will want the PII decryption keys (VITE_COMMUNITY_PRIVATE_KEY, VITE_COMMITTEE_PRIVATE_KEY) and optionally Pinata credentials for IPFS uploads.

Run locally

Two modes depending on your needs:

Solo dev node (recommended for pallet/runtime development)

./scripts/start-dev.sh
# Substrate RPC: ws://127.0.0.1:9944
  • Instant finalisation: blocks produced every 3 seconds, finalised immediately
  • No relay chain: starts in ~30 seconds, no Zombienet overhead
  • Fastest iteration: change code, restart, test in seconds
  • Includes all pallets (Membership, Governance) and genesis data (committee, blocks)

Start the frontend separately:

cd web && npm run dev
# Frontend: https://127.0.0.1:5173

Relay-backed network (full Polkadot stack)

./scripts/start-all.sh
# Substrate RPC: ws://127.0.0.1:9944
# Frontend:      https://127.0.0.1:5173
  • Full relay chain (2 validators) + parachain collator via Zombienet
  • Finalisation takes ~30 seconds (GRANDPA consensus on relay chain)
  • First start takes 5-10 minutes

Incremental deploy (no restart needed)

After making pallet or runtime changes, deploy to the running chain:

./scripts/deploy.sh

This bumps the spec version, builds the runtime WASM, submits a sudo(system.setCode) upgrade, rebuilds the CLI, and regenerates PAPI descriptors from the WASM blob. Works with both dev node and Zombienet. Chain state is preserved.

Run tests

# Membership pallet tests
cargo test -p pallet-membership-gov

# Governance pallet tests
cargo test -p pallet-governance

# All workspace tests
cargo test --workspace

Lint & format

cargo +nightly fmt && cargo clippy --workspace   # Rust
cd web && npm run fmt && npm run lint             # Frontend

Architecture

Clients — React + PAPI web app and a Rust + subxt CLI; both talk to the parachain over WebSocket.

Parachain runtime — Cumulus-based on polkadot-sdk stable2512-3, with two custom pallets:

Pallet Index Surface
Membership 51 Homes, blocks, committee, elections, Q&A, encrypted PII
Governance 52 Proposals, ZK voting, declining quorum, comments

Governance reads membership state through a MembershipProvider trait; nothing flows the other way.

Off-chain pieces the chain delegates to:

  • ZK proofs generated in the browser via WASM (Groth16 over a Poseidon-commitment Merkle tree). Only the verified proof and nullifier hit chain.
  • Proposal media (descriptions, images) pinned to IPFS via Pinata; only CIDs are stored on-chain.
  • PII (names, phones) X25519-encrypted client-side before submission, decrypted in the right audience's browser (members for names, committee for phones).
  • Device sign-in via WebAuthn passkeys + PRF; identity keys derived from a BIP39 mnemonic the user records on first sign-up.

See spec/pallets.md for the full extrinsic surface and spec/web.md for the frontend wiring.

CLI examples

# Apply for membership (unsigned, feeless)
cargo run -p stack-cli -- membership apply --flat-number 101 --block-id 1 --signer //newuser

# Approve an application (committee only)
cargo run -p stack-cli -- membership approve 0x... --signer alice

# List pending applications and committee
cargo run -p stack-cli -- membership list-pending
cargo run -p stack-cli -- membership show-committee

# Create a proposal (description CID pinned to IPFS separately;
# unsigned bare call — `--signer` only identifies the author).
cargo run -p stack-cli -- governance create-proposal --title "Install EV chargers" \
  --description-cid QmXyz... --deadline 1776902400000 \
  --funding reserve --cost 45000 --signer alice

# Tally and close
cargo run -p stack-cli -- governance get-tally <proposal-id>
cargo run -p stack-cli -- governance close-proposal <proposal-id> --signer alice

Run cargo run -p stack-cli -- membership --help and governance --help for the full surface (elections, comments, transfer, device management, candidate Q&A).

Groth16 trusted setup ceremony

Anonymous voting uses Groth16 zero-knowledge proofs, which require a one-time trusted setup. For dev/testing, pre-generated keys are included. For production, run a multi-party ceremony where each participant adds randomness: only one honest participant is needed for soundness.

# 1. Initialise (creates round 0 from a single-party setup)
cargo run -p zk-voting --example ceremony_init --features zk-voting/ceremony,zk-voting/small-tree

# 2. Each participant contributes (sequential: pass output to next person)
cargo run -p zk-voting --example ceremony_contribute --features zk-voting/ceremony \
  -- ceremony/round_000.params ceremony/round_001.params   # participant 1
cargo run -p zk-voting --example ceremony_contribute --features zk-voting/ceremony \
  -- ceremony/round_001.params ceremony/round_002.params   # participant 2
# ... repeat for 10-20 participants

# 3. Finalize: extract keys and verify with a test proof
cargo run -p zk-voting --example ceremony_finalize \
  --features zk-voting/ceremony,zk-voting/small-tree \
  -- ceremony/round_002.params blockchain/primitives/zk-voting/test-keys

# 4. Deploy
cp blockchain/primitives/zk-voting/test-keys/pk.bin web/public/zk-proving-key.bin
# Rebuild chain (VK in genesis) and frontend

Each contributor's randomness is generated from OS entropy and never touches disk. See spec/crypto.md for the full cryptographic design.

PII encryption keys

Resident names and phone numbers are encrypted on-chain in the membership pallet (blockchain/pallets/membership/) using X25519 hybrid encryption (ECDH + HKDF-SHA256 + AES-256-GCM). Two keypairs are needed: one for names (all members can decrypt) and one for phone numbers (committee-only).

# Generate both keypairs (uses Web Crypto / Node.js >= 20)
node --input-type=module -e "
import { webcrypto } from 'node:crypto';
const { subtle } = webcrypto;
for (const label of ['community', 'committee']) {
  const kp = await subtle.generateKey('X25519', true, ['deriveBits']);
  const pub = Buffer.from(await subtle.exportKey('raw', kp.publicKey)).toString('hex');
  const pkcs8 = Buffer.from(await subtle.exportKey('pkcs8', kp.privateKey));
  const priv = pkcs8.subarray(16, 48).toString('hex');
  console.log(label + '_public:  ' + pub);
  console.log(label + '_private: ' + priv);
}
"

Deploy the public keys on-chain (set in genesis config or via committee extrinsic):

Private keys are distributed to devices via the app and stored in localStorage:

localStorage key Who holds it How it's set
gov-community-private-key All members Device pairing QR (PairDevicePage.tsx) or dev login (SignInPage.tsx)
gov-committee-private-key Committee only Key rotation UI (MembershipPage.tsx) or dev login
  • The pairing QR includes the community private key alongside home_secret (built in MyHomePage.tsx)
  • Committee private keys are shared via the "Encryption Keys" section on the Membership page
  • Dev keys are defined in web/src/config/devKeys.ts and set automatically on dev login

Key rotation: committee members can rotate either keypair from the Membership page. Rotating the committee key triggers rotate_encrypted_phones to re-encrypt all phone numbers in batches.

Genesis demo data

Genesis homes (Alice / Bob / Charlie) ship with pre-encrypted phone numbers so the committee table on a fresh chain matches production behaviour rather than relying on a plaintext fallback. The plaintext numbers and home IDs live in blockchain/genesis-content/homes.json; scripts/pin-genesis-content.mjs re-encrypts them with the dev committee public key (matching the committee_public_key in genesis) and writes the resulting *_PHONE_CIPHERTEXT constants into blockchain/runtime/src/genesis_content.rs. Use --skip-ipfs to regenerate phone ciphertexts without re-pinning to Pinata:

node scripts/pin-genesis-content.mjs --skip-ipfs

See spec/crypto.md for the full PII encryption design.

Documentation

Design specification (spec/):

  • model.md — domain model: home, block, committee, proposal, vote, comment
  • voting.md — voting rules: declining quorum, anonymity model, pass/fail logic, proposal closure
  • pallets.mdpallet-membership-gov and pallet-governance: storage, extrinsics, events, cross-pallet wiring
  • web.md — frontend design: routes, passkey + PRF authentication, multi-device pairing, ZK voting flow
  • crypto.md — cryptographic design: Groth16 voting, PII encryption, key rotation
  • plan.md — implementation plan

Setup and tooling (docs/):

Built with

  • Polkadot SDK — Cumulus parachain runtime, FRAME pallets
  • polkadot-omni-node — block author for the local dev node
  • Zombienet — relay chain + collator orchestration for local testing
  • PAPI — frontend Substrate client
  • subxt — CLI Substrate client
  • arkworks — Groth16 + Poseidon for ZK voting (compiled to WASM for in-browser proving)
  • WebAuthn + PRF — passkey sign-in with at-rest key sealing
  • Pinata — IPFS pinning for proposal descriptions and images
  • React + Vite + Tailwind — frontend stack

Pinned versions live in Cargo.toml, web/package.json, and rust-toolchain.toml.

License

MIT

About

Decentralised governance platform for homeowners

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors