Skip to content

stackdump/bitwrap-io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

127 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bitwrap

CI jsDelivr

Anonymous on-chain voting with ZK proofs.

Create polls where every vote is backed by a Groth16 proof. No one sees how you voted. Everyone can verify the result is correct. Deploy to any EVM chain.

bitwrap.io | Polls | Editor | Docs

Quick start

Prerequisites: Go ≥ 1.24. Everything else is optional — forge + anvil (Foundry) are only needed for the make validate pipeline that deploys generated contracts end-to-end; npm is only for the Playwright e2e suite.

git clone https://github.com/stackdump/bitwrap-io.git
cd bitwrap-io
make run    # serves on :8088

Three API calls to create a poll, cast a vote, and read results:

# Create a poll (requires wallet signature)
curl -X POST https://api.bitwrap.io/api/polls \
  -H "Content-Type: application/json" \
  -d '{"title":"Board Vote","choices":["Approve","Reject","Abstain"],...}'

# Cast a ZK-proven vote
curl -X POST https://api.bitwrap.io/api/polls/{id}/vote \
  -H "Content-Type: application/json" \
  -d '{"nullifier":"0x...","voteCommitment":"0x...","proof":"..."}'

# Get results (sealed while active, visible when closed)
curl https://api.bitwrap.io/api/polls/{id}/results

How it works

Vote. Voters register with a commitment hash. When they cast a ballot, a nullifier (derived from mimcHash(voterSecret, pollId)) prevents double-voting without revealing identity.

Prove. Each vote generates a Groth16 proof attesting the voter is registered and the ballot is valid — without revealing the choice. The circuit verifies Merkle inclusion in the voter registry, nullifier binding, and vote range.

Tally. Results stay sealed until the poll closes. Once closed, the final tally is publicly verifiable — anyone can audit the proofs without accessing individual ballots.

Sealed results

While a poll is active, the results endpoint only returns the vote count. Tallies, nullifiers, and commitments are hidden to prevent observers from diffing the tally after each vote and correlating timing to de-anonymize voters. Full results are exposed only after the poll is closed.

Wallet-native auth

Poll creation requires an EIP-191 personal_sign signature from MetaMask or any Ethereum wallet. Voting secrets can be derived from wallet signatures, making the nullifier deterministic per voter per poll — no accounts, no passwords.

What it does

  • ZK voting — anonymous polls with nullifier-based double-vote prevention and on-chain proof verification. Sealed results prevent vote-timing correlation.
  • Visual editor — draw places, transitions, and arcs in the browser. Models are stored as content-addressed JSON-LD.
  • Solidity generation — produce deployable contracts and Foundry test suites from any template.
  • ZK circuits — Groth16 circuits for transfer, mint, burn, approve, transferFrom, vestClaim, and voteCast, all generated from the Petri net schema (no hand-written gnark). One .btw source produces Solidity + Foundry tests + ZK circuits + witness builders.
  • Deploy bundle — download complete Foundry projects for v1/v2 (GET /api/bundle/vote) and v3 homomorphic settlement (GET /api/bundle/vote-v3).
  • ERC templates — start from ERC-20, ERC-721, ERC-1155, or a Vote template. Each is a complete Petri net with guards, arcs, and events.
  • .btw DSL — a compact schema language for defining Petri net models.
  • Remix IDE plugin — generate and deploy contracts inside Remix at solver.bitwrap.io.

API

Polls

POST /api/polls              Create poll (wallet signature required)
GET  /api/polls              List polls
GET  /api/polls/{id}         Get poll details
POST /api/polls/{id}/vote    Cast ZK-proven vote
POST /api/polls/{id}/close   Close poll (creator signature required)
POST /api/polls/{id}/reveal  Reveal vote choice (post-close)
GET  /api/polls/{id}/results Poll results (sealed while active)

Models & templates

POST /api/save               Save JSON-LD model, returns {"cid": "..."}
GET  /o/{cid}                Load model by CID
GET  /img/{cid}.svg          Render model as SVG
POST /api/svg                Generate SVG from posted JSON-LD
GET  /api/templates          List ERC templates
GET  /api/templates/{id}     Get full template model
POST /api/solgen             Generate Solidity from template
POST /api/testgen            Generate Foundry tests from template
POST /api/compile            Compile .btw DSL to schema JSON
GET  /api/bundle/{template}  Download Foundry project (ZIP)
                            - /api/bundle/vote: v1/v2 poll + voteCast verifier
                            - /api/bundle/vote-v3: BitwrapZKPollV3 + voteCastHomomorphic_8 + tallyDecrypt_8 verifiers

Operator tools

POST /api/polls/{id}/aggregate  Close a v3 poll (signed aggregate proof required)
GET  /api/polls/{id}/tally      Download the tally artifact (post-close)

A v3 poll can also be closed from the command line by a server operator who has the creator's BabyJubJub secret key (--sk-hex) and their Ethereum private key (--eth-key) or a pre-computed EIP-191 signature (--signature):

# Step 1: compute tallies and print the payload that needs to be signed
bitwrap close-poll <pollId> \
    --sk-hex=<creatorBabyJubJubSecretKeyHex> \
    --server=https://bitwrap.io

# Step 2: re-run with the signature once you have it
bitwrap close-poll <pollId> \
    --sk-hex=<hex> \
    --signature=<eip191sig> \
    --server=https://bitwrap.io

# Or: sign internally with an Ethereum private key (single step)
bitwrap close-poll <pollId> \
    --sk-hex=<hex> \
    --eth-key=<ethPrivKeyHex> \
    --server=https://bitwrap.io

# Optional: cache the compiled tallyDecrypt_8 circuit for fast restarts
bitwrap close-poll <pollId> --sk-hex=<hex> --eth-key=<hex> \
    --key-dir=./keys --server=https://bitwrap.io

The subcommand:

  1. Fetches /api/polls/{id}/votes — the per-vote ciphertext list (one record per ballot).
  2. Aggregates the ciphertexts per-bin across all ballots.
  3. Decrypts each bin with the BabyJubJub secret key to recover the tally vector.
  4. Compiles tallyDecrypt_8 locally and runs groth16.Prove (Go-side — no WASM).
  5. POSTs {creator, signature, tallies, decryptProofBytes} to /api/polls/{id}/aggregate.

v1/v2 polls are rejected with an informative error. This is the recommended path until browser-side close lands.

For production use, prefer file-based or env-based key sources to avoid putting secret bytes in shell history:

# File-based (mode 0o600 strongly recommended on the key files)
bitwrap close-poll <pollId> \
    --sk-hex-file=./creator-bjj.key \
    --eth-key-file=./creator-eth.key \
    --server=https://bitwrap.io

# Or via environment
BITWRAP_SK_HEX=<hex> BITWRAP_ETH_KEY=<hex> bitwrap close-poll <pollId> --server=...

ZK prover

GET  /api/circuits              List available circuits
POST /api/prove                 Submit witness for proof generation
GET  /api/vk/{circuit}          Download verifying key (binary)
GET  /api/vk/{circuit}/solidity Download Solidity verifier contract

For v3, the bundle includes generated Solidity verifiers and BitwrapZKPollV3 wiring for close-time settlement with the aggregate artifact (data/polls/{id}/tally.json).

CDN

Client-side modules are available via jsDelivr:

<script type="module">
import { mimcHash } from 'https://cdn.jsdelivr.net/gh/stackdump/bitwrap-io@latest/public/mimc.js';
import { MerkleTree } from 'https://cdn.jsdelivr.net/gh/stackdump/bitwrap-io@latest/public/merkle.js';
import { buildVoteCastWitness } from 'https://cdn.jsdelivr.net/gh/stackdump/bitwrap-io@latest/public/witness-builder.js';
</script>
Module Description
mimc.js MiMC-BN254 hash (pure BigInt, zero deps)
merkle.js Fixed-depth binary Merkle tree with proof generation
witness-builder.js Witness builders for all 7 ZK circuits
petri-view.js <petri-view> web component for Petri net editing

Architecture

Single Go binary. Vanilla JS frontend. No npm, no React, no build step.

cmd/bitwrap/       Entry point (flags: -port, -data, -compile, -synthesize, -validate)
                   Subcommands: close-poll
dsl/               .btw lexer, parser, AST, builder
erc/               ERC token standard templates (020, 721, 1155, 4626, 5725, vote)
prover/            Groth16 prover service + generated circuits (prover/*_gen.go)
  synth/           Circuit synthesizer — metamodel.Schema → gnark source
solidity/          Solidity contract + test generation
internal/
  server/          HTTP handlers + poll lifecycle + wallet auth
  petri/           Petri net runtime (places, transitions, arcs, firing, reachability)
  metamodel/       Schema types (states, actions, arcs, guards, events, ZKOps)
  metamodel/guard/ Guard expression parser + evaluator
  seal/            CID computation (JSON-LD canonicalization via URDNA2015)
  store/           Filesystem storage (polls, votes, events)
  svg/             SVG rendering
public/            Frontend JS/CSS/HTML (embedded via go:embed)
arc/               (deprecated) — compat wrappers re-exporting internal/petri

.btw schema language

Models can also be written as .btw files — a compact DSL for defining Petri net state machines. Here's a simplified poll:

schema Poll {
  version "1.0"

  register voterRegistry map[uint256]uint256 observable
  register nullifiers map[uint256]bool observable
  register tallies map[uint256]uint256 observable

  event VoteCast {
    nullifier: uint256 indexed
    choice: uint256
  }

  fn(castVote) {
    var nullifier uint256
    var choice uint256
    var weight uint256

    require(nullifiers[nullifier] == false)
    @event VoteCast

    castVote -|weight|> tallies
    castVote -|weight|> nullifiers
  }
}

The arrows (-|weight|>) are arcs — they describe how tokens flow through the net. castVote increments the tally for the chosen option and marks the nullifier as used, all in one atomic transition.

Compile to JSON: bitwrap -compile poll.btw

Circuit synthesis

The same .btw source also drives the ZK circuits. A Petri net's structure maps naturally to Groth16 constraints:

Petri net primitive Circuit constraint
Keyed input arc (BALANCES[from] -|amount|>) Merkle membership proof in pre-state tree
Keyed output arc (-|amount|> BALANCES[to]) Post-state commitment hash
Guard (BALANCES[from] >= amount) Range check via ToBinary
Role (fn(mint) requires minter) caller == minter equality
ZKOp (nullifier-bind, commitment-bind) Keccak/MiMC derivation constraint

Run bitwrap -synthesize examples/erc20.btw and you get Go source for TransferCircuit, ApproveCircuit, MintCircuit, BurnCircuit, and TransferFromCircuit — each a gnark frontend.Circuit with correct public/private variable partitioning, Merkle-proof arrays sized per State.MerkleDepth, and guard-derived range checks. The Groth16 verifying key ships alongside the Solidity verifier so on-chain verification matches off-chain proving byte-for-byte.

There is no hand-written gnark code in this repo — prover/circuits.go is 70 lines of registration glue, and every circuit type lives in prover/*_gen.go. A make gen-circuits && git diff --exit-code check in CI catches stale generated output.

License

MIT

About

Anonymous on-chain voting with ZK proofs — create polls, cast verifiable ballots, tally results. Built on Petri net state machines.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors