313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# ADR 005 — Git-native, anonymous voting system
|
|
|
|
**Date:** 2026-05-11
|
|
**Status:** Proposed
|
|
|
|
---
|
|
|
|
## Context
|
|
|
|
As the agency scoring system matures, the community will need to make two kinds of
|
|
binding decisions:
|
|
|
|
**Governance proposals** — changes to the scoring system itself: adding or removing data
|
|
sources, adjusting platform weights, changing eligibility criteria. These define what
|
|
"participation" means and directly affect contributor scores.
|
|
|
|
**General community proposals** — decisions about project direction, technical choices,
|
|
resource allocation, or any other matter the community wishes to put to a vote. These
|
|
are not specific to the scoring system; the voting infrastructure is general-purpose.
|
|
|
|
Both types require a process that is legitimate, tamper-resistant, and accessible to any
|
|
community that forks this system. The mechanism is the same for both; only the vote
|
|
weighting differs — see [Proposal types and vote weighting](#proposal-types-and-vote-weighting).
|
|
|
|
Several constraints shape the design:
|
|
|
|
- **Fork-friendly** — any community adopting this system should get the voting infrastructure
|
|
for free, without depending on OSArch's servers, keys, or accounts
|
|
- **Public data only** — consistent with [ADR 003](003-public-data-only.md); no dependency
|
|
on private APIs or authenticated services
|
|
- **No central authority** — no single person or role should be able to unilaterally
|
|
determine an outcome
|
|
- **Anonymous ballots** — in small, tight-knit communities, public voting creates social
|
|
pressure that distorts honest signal; contributors should be able to vote their genuine
|
|
view without fear of social consequence
|
|
- **Verifiable** — anyone should be able to independently verify that votes are legitimate
|
|
and the tally is correct
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
**Votes are anonymous, cryptographically verifiable, and stored entirely in git.**
|
|
|
|
The mechanism is a **linkable ring signature** — the same cryptographic primitive used
|
|
by Monero for private transactions. Combined with a community norm for vote submission,
|
|
this achieves full ballot anonymity without requiring any external infrastructure.
|
|
|
|
---
|
|
|
|
## Why other approaches were rejected
|
|
|
|
**Public voting (PR reactions, forum polls with visible results)**
|
|
The most obvious approach — and the most problematic. In a small community where
|
|
contributors know each other and score rankings are visible, public voting creates implicit
|
|
coercion. People vote with influential contributors, avoid opposing popular proposals, and
|
|
self-censor minority views. Secret ballots exist in political systems for exactly this reason.
|
|
The dynamics are no different here.
|
|
|
|
**Platform polls (hidden-results forum polls)**
|
|
Some forum platforms support polls with results hidden until close, which addresses the
|
|
social pressure problem during voting. But this is not fork-friendly — communities use
|
|
different forum platforms or none at all, and features vary. The vote record lives outside
|
|
git, making it harder to audit and impossible to reproduce independently. Rejected as a
|
|
primary mechanism; acceptable as an informal discussion tool during the proposal phase.
|
|
|
|
**Commit-reveal**
|
|
An improvement over public voting — voters submit a hash of their vote during the voting
|
|
period, then reveal after it closes. Prevents bandwagon effects and hides the running tally.
|
|
But the reveal phase is still public: observers know Alice voted yes and Bob voted no.
|
|
In a small electorate this is meaningful information. Ring signatures provide stronger
|
|
guarantees at acceptable complexity.
|
|
|
|
**On-chain voting (smart contracts, DAO tooling)**
|
|
Maximally tamper-resistant but introduces dependencies (gas costs, wallet management,
|
|
specific blockchain infrastructure) that make the system inaccessible to most communities
|
|
and impossible to self-host simply. Reserved as a future direction if the community grows
|
|
to a scale where on-chain guarantees are worth the overhead.
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
### Cryptographic foundation
|
|
|
|
Each eligible voter holds a private key. When casting a vote, they produce a
|
|
**linkable ring signature** over the set of all eligible voters' public keys. This signature:
|
|
|
|
- Proves the vote came from *someone* in the eligible set (verifiable by anyone)
|
|
- Reveals nothing about *which* voter cast it (unlinkable)
|
|
- Produces a **key image** — a deterministic value derived from the voter's private key
|
|
that is the same for any two votes from the same key, enabling double-vote detection
|
|
without identity disclosure
|
|
|
|
### Vote file format
|
|
|
|
```json
|
|
{
|
|
"proposal": "add-new-platform",
|
|
"proposal_hash": "8f3a...",
|
|
"choice": "yes",
|
|
"ring_signature": "...",
|
|
"key_image": "8f3a..."
|
|
}
|
|
```
|
|
|
|
No username. No identity. The proposal hash locks the vote to a specific version of the
|
|
proposal — votes cast against an amended proposal are distinct from earlier votes.
|
|
|
|
### Vote lifecycle
|
|
|
|
**1. Proposal phase**
|
|
A contributor opens a PR adding a file to `docs/sites/proposed/{proposal-name}/proposal.md`.
|
|
A community discussion period opens (on whatever channel the community uses). A minimum
|
|
discussion period (recommended: 14 days) must elapse before voting opens.
|
|
|
|
**2. Vote opens — eligible voter snapshot**
|
|
When voting begins, the current eligible voter list (public keys of all contributors
|
|
meeting the eligibility criteria) is committed to:
|
|
`docs/sites/proposed/{proposal-name}/eligible-voters.json`
|
|
|
|
This snapshot is locked. Late score changes do not affect eligibility for this vote.
|
|
|
|
**3. Commit phase — vote creation**
|
|
Each eligible voter runs locally:
|
|
```
|
|
python src/voting/vote.py create --proposal add-new-platform --choice yes
|
|
→ Vote file saved to: ~/.agency/votes/add-new-platform.vote
|
|
```
|
|
The vote file never touches the repo directly from the voter.
|
|
|
|
**4. Submission — community norm**
|
|
Voters share their vote files with other community members for submission. Three
|
|
equivalent approaches:
|
|
|
|
- **Peer submission** — send your vote file to a trusted community member; they commit it
|
|
- **Drop channel** — post your vote file to a designated public channel (chat room,
|
|
forum thread); anyone collects and commits
|
|
- **Returning officer** — a rotating, temporary role; one community member per vote
|
|
collects all files from the drop channel and commits them in batches
|
|
|
|
The returning officer cannot fabricate votes (ring signatures would fail verification)
|
|
but could suppress them. This is mitigated by the drop channel being public — anyone
|
|
can count files posted versus files committed. Any discrepancy is visible and challengeable.
|
|
The role rotates; no permanent privileged position.
|
|
|
|
**5. Tally**
|
|
Anyone runs:
|
|
```
|
|
python src/voting/vote.py tally --proposal add-new-platform
|
|
→ 12 valid votes: 9 yes, 3 no
|
|
→ Quorum: 12/20 eligible (60%) ✓
|
|
→ Result: PASSED
|
|
```
|
|
Deterministic. Anyone gets the same result. No trusted tallier required.
|
|
|
|
**6. Result committed**
|
|
The tally result is committed to `docs/sites/proposed/{proposal-name}/result.md`.
|
|
The proposal file moves to `docs/sites/active/` or `docs/sites/retired/` accordingly.
|
|
|
|
### Git record for a complete vote
|
|
|
|
```
|
|
commit "vote: open add-new-platform — eligible voter snapshot"
|
|
commit "vote: add-new-platform batch 1 — 6 votes (returning officer: contributor-a)"
|
|
commit "vote: add-new-platform batch 2 — 6 votes (returning officer: contributor-a)"
|
|
commit "vote: add-new-platform PASSED 9/12 (75%) — result committed"
|
|
commit "docs: move add-new-platform from proposed/ to active/"
|
|
```
|
|
|
|
---
|
|
|
|
## Proposal types and vote weighting
|
|
|
|
Not all proposals are equal in stakes or scope. Two distinct vote modes are defined:
|
|
|
|
**Governance proposals** — changes to the scoring system itself: adding or removing data
|
|
sources, adjusting platform weights, changing eligibility criteria, modifying the scoring
|
|
formula. For these, votes are **equal weight above the eligibility threshold**. High scorers
|
|
should not have disproportionate power over the rules that produced their score. Equal
|
|
voting above a participation gate prevents the system from being captured by whoever
|
|
currently benefits from it most.
|
|
|
|
**General community proposals** — anything outside the scoring system: project direction,
|
|
technical decisions, resource allocation, community initiatives. For these, votes are
|
|
**weighted by agency score**. The score is the whole point of this system — a signal of
|
|
sustained participation. Weighting general votes by score is precisely what the agency
|
|
signal is designed to enable.
|
|
|
|
This distinction is the reason the system exists. It produces a participation signal
|
|
(score-weighted decisions) while protecting the integrity of that signal (equal-weighted
|
|
governance). Conflating the two modes would allow either capture (high scorers define
|
|
their own rules) or under-representation (everyone gets equal say on decisions where
|
|
experience and contribution depth genuinely matter).
|
|
|
|
The vote file format, ring signature mechanism, and git lifecycle are identical for both
|
|
types. The difference is in how the tally script aggregates results: equal count vs.
|
|
score-weighted sum.
|
|
|
|
---
|
|
|
|
## Eligibility
|
|
|
|
A single score threshold is insufficient — it addresses low-effort sybils but not a
|
|
determined actor maintaining multiple high-scoring identities. Eligibility requires
|
|
all three:
|
|
|
|
| Criterion | Purpose |
|
|
|---|---|
|
|
| Score above threshold (TBD) | Excludes low-commitment accounts |
|
|
| Activity spanning at least N platforms | Makes multi-identity maintenance costly — platform accounts verified via [ADR 006](006-cross-platform-identity-attestation.md) |
|
|
| Activity spanning at least M months | Prevents last-minute score farming |
|
|
|
|
The specific values for threshold, N, and M are community decisions to be made before
|
|
the first real vote. They should be committed to `config.yaml` and subject to the same
|
|
governance process as weight changes.
|
|
|
|
Different proposal types warrant different thresholds — adding a data source is lower
|
|
stakes than removing one; changing the scoring formula is the highest-stakes change.
|
|
A tiered threshold system is recommended but left for the community to define.
|
|
|
|
---
|
|
|
|
## Tooling
|
|
|
|
All scripts live in `src/voting/`:
|
|
|
|
| Script | Purpose |
|
|
|---|---|
|
|
| `vote.py keygen` | Generate a voting key pair |
|
|
| `vote.py create` | Create a ring-signed vote file |
|
|
| `vote.py verify` | Verify a single vote file |
|
|
| `vote.py tally` | Tally all votes for a proposal |
|
|
| `snapshot.py` | Generate eligible voter snapshot at vote open |
|
|
|
|
All inputs and outputs are files. All outputs are committed to git. No server required.
|
|
|
|
---
|
|
|
|
## Trust surface
|
|
|
|
| Trust assumption | Mitigation |
|
|
|---|---|
|
|
| Eligible voter snapshot is accurate | Public, auditable before voting opens |
|
|
| Returning officer submits all received votes | Drop channel is public; posted vs committed count is visible |
|
|
| Score calculation is honest | Anyone can re-run the script independently |
|
|
| Ring signature library is correct | Use audited, well-established implementation |
|
|
|
|
---
|
|
|
|
## Bootstrap problem
|
|
|
|
Before the system has live scores, no contributor meets the eligibility criteria. The
|
|
first votes — including votes on the eligibility criteria themselves — require a founding
|
|
contributor set. This set must be defined explicitly at genesis and committed to the repo
|
|
as a named, dated file. It expires once the system produces live scores.
|
|
|
|
Rather than a politically-chosen insider list, the recommended approach is to derive the
|
|
founding set from existing, publicly verifiable community signals:
|
|
|
|
**Primary signal — forum gamification score:** Contributors above a defined threshold on
|
|
the community forum (e.g. 30+ points in the platform's built-in points system) are
|
|
included. This is publicly verifiable by anyone, requires no insider decision, and reflects
|
|
accumulated standing in the community's primary discussion venue.
|
|
|
|
**Supplementary signal — platform activity fallback:** Contributors who are active on
|
|
other tracked platforms (e.g. above a minimum GitHub contribution count) but have low
|
|
or no forum presence are also included. This prevents the founding set from excluding
|
|
developers whose primary contribution is code rather than discussion.
|
|
|
|
**Calibration note:** The specific threshold (e.g. 30 points) should be chosen by
|
|
inspecting the actual distribution of scores in the community before it is committed.
|
|
A threshold that includes 200 people and one that includes 20 produce very different
|
|
founding electorates. The goal is "meaningfully active member," not "most active member."
|
|
|
|
**Important caveat:** Using the forum's built-in gamification score at bootstrap is a
|
|
pragmatic one-time choice, not an endorsement of that system as a long-term signal.
|
|
Vanilla's (or any forum platform's) gamification formula is opaque and outside community
|
|
control — see [community-osarch.md](../sites/active/community-osarch.md) for why it is
|
|
excluded from ongoing score calculation. At genesis, the formula doesn't need to be
|
|
perfect; it needs to be a reasonable, verifiable proxy for community standing. Once the
|
|
agency system produces live scores, the bootstrap set expires and the formal eligibility
|
|
criteria take over.
|
|
|
|
The founding set, the threshold used, and the date it was generated must all be committed
|
|
to the repo with a clear note that this represents a bootstrap, not permanent special
|
|
status. Any contributor who qualifies under the live system but missed the bootstrap
|
|
snapshot gains full eligibility automatically once live scores are available.
|
|
|
|
---
|
|
|
|
## Limitations
|
|
|
|
- **Small electorate inference** — with few eligible voters, even anonymous ballots leak
|
|
information through elimination. No cryptographic scheme fully solves this for small groups.
|
|
- **Sybil resistance** — ring signatures prevent the same key from voting twice but cannot
|
|
prevent a person with multiple legitimately-registered keys from voting multiple times.
|
|
See [ADR 006](006-cross-platform-identity-attestation.md) for the identity layer that
|
|
addresses this.
|
|
- **Anonymity of submission, not of participation** — observers know you submitted a vote
|
|
file (or asked someone to), even if they can't read the contents. Full participation
|
|
anonymity is not achievable.
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
- The voting system ships with the repo — every fork gets it for free
|
|
- No external service required for any part of the process except the platform APIs
|
|
already used for scoring
|
|
- The trust surface is small, named, and publicly auditable
|
|
- The system can be adopted incrementally — [human attestation](006-cross-platform-identity-attestation.md)
|
|
and git-tracked proposals work independently before ring signature tooling is built
|