Skip to content
Condorcet.vote Alpha
← Technical documentation

Public register

What every election makes public, whatever carries it, and what anyone can verify from it without trusting the platform.

The register is the verifiable record of the election. The platform is a trusted third party at vote time; everything it did is checkable afterwards from the published register alone. This page describes its content, independently of its transport: the blockchain is only one way of distributing it, the one that lets anyone check its integrity and immutability.

Protocol v1

Content

Five pieces, two unlinkable sides.

A definition and rings, shared by the whole election, then two deliberately separate registers: the participation register says who took part, the ballot register says what was voted, with its cryptographic proof and no trace of by whom or when. The result follows from them.

  • Context

    Election definition

    Title, organizer, type, seats, candidates, ballot rules and privacy options, frozen before the first ballot.

    At opening · configuration block

  • Context

    Rings

    The complete list of public keys allowed to sign, ring by ring, with each ring’s digest.

    At opening, then per emergency ring · rings blocks

  • Participation side

    Participation register

    One line per voter, under a random identifier: voted or not, in which ring, how many keys received, what weight. Pseudonymous by default.

    At close, as a single snapshot · closure block

  • Ballot side

    Signed ballots

    Every ballot as signed, byte for byte, with its ring signature and key image, with no identity and no timestamp.

    During the vote, in batches of at least 600 · votes blocks

  • Computation

    Published result

    The official ranking, with the recorded method and computation versions, and the ballots in CEF format. Never a block: it is recomputed.

    When the organizer publishes · results page

  • Outside the register

    Never published

    Timestamps, arrival order, the mapping between identifier and identity (unless the nominative option is on), cosmetic presentation: logo, photos, thank-you message.

    Neither on the page nor in a block

Definition & rings

What is frozen before the first ballot.

The definition is frozen when the election is launched; each ring is frozen when it is created. Neither changes afterwards: every ballot is verified against the published definition and ring.

Election definition

protocol_version integer
The protocol version, 1 today, bound to the election forever.
election_id UUID v4
The election identifier, repeated in every signed ballot and in every signature check.
title, description?, organizer_name strings
Informative context published in clear, never a tally input.
election_type, seats preferential | fptp, integer ≥ 1
Ranking or single choice; number of seats, hence of candidates elected from the top of the ranking.
candidates[].label array
The candidate labels, the sole identifier, sorted by label.
ballot_rules object
The ballot parameters (ties, partial ranking, blank vote, full ranking), from which the tally constraints are derived by a documented rule.
non_anonymous_voting, nominative_participation, weighted_voting booleans
The privacy and weighting options, off by default. Non-anonymous voting forces the nominative participation register.
max_ring_size, initial_electoral_roll_size integers
The ring-size ceiling (600 today) and the size of the electoral roll at opening.

The field-by-field detail, including voter_access_mode and enrolment_mandatory, is in the blockchain specification. The Condorcet method is not part of the definition: it is published with the result.

Rings

ring_id UUID
The ring a ballot is signed against; every ballot names it.
public_keys[] pk_<64 hex>_<8 hex>
All the ring’s public keys, unique, sorted ascending, at most max_ring_size.
ring_digest ring_<64 hex>_<8 hex>
The canonical, order-independent fingerprint of the member set, recomputable from the keys.

A ballot’s anonymity is exactly the ring it was signed under: a platform serving a voter a ring that differs by a single member could recognize that voter’s ballot. The voting page computes the digest from the ring it was served (it never receives one from the platform), shows it before signing and writes it into the receipt; the voter compares it with the value published here for everyone. This is detection, not prevention.

An emergency ring, created during the vote if the roll grows, is published when it is created. Every ring can be downloaded as JSON from the register page, with its digest and its keys in the same order as in its block.

Participation register

Who took part, published once.

The participation register is published as a single snapshot, in the closure block, once the roll is final. During the vote the register page shows none of it: no participation line ever shares a publication window with a ballot.

One line per voter

voter_id random UUID v4
The line’s only identifier; lines sorted by voter_id. The same across every election of a group.
key_provisioning_mode server_generated | organizer_imported | voter_enrolled
Where the key comes from: generated by the platform, imported by the organizer, or brought by the voter.
has_voted boolean
Voted or not.
ring_id UUID or null
The ring the voter voted in, null if they did not vote: the voter → ring link behind the per-ring check.
distribution_count integer ≥ 0
How many voting keys were distributed: at most 3 by default, more only if the organizer grants extras, which stays visible. Individual distributions are not itemized.
weight integer ≥ 1
The weight (shares), always present, 1 without weighted voting: the value the weight signed in the ballot is checked against.

The closing roll includes voters added during the vote: its size minus initial_electoral_roll_size gives how many, never which. The closure block also carries the totals: blocks, ballots, rings, voters, voted weight.

Nominative participation: an option

Off by default, the organizer can turn it on at launch, with voters notified; non-anonymous voting forces it. Only the lines of voters who voted then get two more fields.

voter_name string
The name in clear, as the organizer supplied it at import.
email_digest 64 hex characters
SHA3-256(election_id ‖ normalized email address), lowercase hex: a digest specific to the election.
  • Abstainers are never named. Their line stays pseudonymous: the option reveals who voted, never who abstained.
  • A participation disclosure, not a ballot disclosure. On its own it links a name to having voted, never to a ballot. With non-anonymous voting, which puts the voter identifier inside the signed ballot, the link becomes ballot → identifier → name.
  • A weak digest, by design. The salt (election_id) is public and an email address can be guessed: anyone holding an address can confirm its owner’s participation and link it across elections. It is a roll-auditing handle, not a protection; it remains personal data.
Ballots

What was voted, without by whom or when.

One record per ballot, sorted by key image: a deterministic order derived from the content, unrelated to arrival order.

One record per ballot

ring_id UUID
The ring whose keys verify the signature.
payload JSON string
The signed payload, verbatim, byte for byte: version, election, ballot expression, weight, and the voter identifier in non-anonymous voting.
signature blsag_…
The ring signature (bLSAG) over the payload.
key_image ki_…
The key image: the same key always yields the same image within an election, without revealing which of the ring’s keys signed.
Signed ballot specification
  • Batch publication. Ballots are published in batches of at least 600, on a randomized time window; an undersized batch waits for the next one. Everything is published at close, and the last batch may hold fewer than 600 ballots: an election with fewer than 600 voters therefore publishes its ballots at close.
  • One rule for the page and the chain. The register page and the blockchain read the same flag, is_chain_published: they always show exactly the same set of ballots. During the vote the page shows the number of ballots received and awaiting publication, without their content.
  • Finding one’s ballot before publication. The key-image lookup (a rate-limited POST) also finds a ballot received but not yet published. A key image cannot be guessed: only the receipt holder can submit it. The public list itself shows only published ballots.
  • One page per published ballot. Payload, signature, key image and the digest of its ring. The ballot identifier the page shows is a random UUID v4; it is not in the blocks.
Timeline

When each piece appears.

Nothing is public before the vote opens. After that the register only grows, until the close that terminates it.

By election state

Draft, Enrolment, Opening Preparation
Nothing public. At Opening the definition and the launch rings are frozen and sealed.
Open Vote open
The register page is online: definition, rings, ballots published in batches, number of pending ballots, key-image lookup. An emergency ring appears when it is created. No participation register.
Closed Vote closed
The last batch of ballots, then the closure block: full participation register and totals. No block can follow it.
Published Result published
The organizer publishes the result: results page and CEF download.
Archived Archived
The register stays readable, read-only; the result is no longer shown publicly.
Cancelled Cancelled
Every public view is withdrawn and the register page says so. No closure block is emitted: blocks already published stay valid, the chain simply stops.

The published result

  • Never a block. The result is recomputed from the published definition and ballots; it is not chain data.
  • Method and versions recorded. The result records the method used, the CondorcetPHP library version and the tally contract version, so a recount replays exactly the same semantics. The results page shows the method and the library version.
  • The ballots in CEF format. Downloadable from the results page: a convenience, rebuildable from the register alone, which a verifier can regenerate to compare.
  • The elected. The top seats positions of the recomputed ranking, nothing more.
Tally & recount specification
Page & blockchain

The same data, two readings.

The register page is the primary, paginated and searchable view of the published data; it is not immutable. The blockchain is one way of distributing that same data: hash-chained JSON files signed by the platform, which anyone can download and keep. The page embeds it as a subsection.

Public addresses per election

/elections/{election}/register
The register page: election state, counters, participation register, rings, lookup, ballots, blockchain.
POST /elections/{election}/register/lookup
The ballot lookup by key image, rate-limited; the key image never appears in a URL.
/elections/{election}/register/ballots/{ballot}
The detail of a published ballot.
/elections/{election}/register/rings/{ring}/keys
A ring as JSON: election_id, ring_id, ring_number, ring_digest, public_keys.
/elections/{election}/results
The result, in the Published state only.
/elections/{election}/results/ballots.cef
The ballots in CEF format, under the same condition.
/elections/{election}/chain
The blockchain manifest, leading to every block and its sidecar files.

These addresses carry no language prefix and are not indexed by search engines. They exist while the election has a public register, from Open to Archived.

What the blockchain adds

  • The same data. Four block types (configuration, rings, votes, closure) carrying exactly the content described on this page.
  • Integrity. Each block carries the SHA3-512 hash of the previous one and a detached platform signature: a retroactive edit breaks the chain for every later observer.
  • Immutability, to be checked. The signature proves origin, not immutability: the platform holds its key. Copies kept by third parties, and the Bitcoin anchoring through OpenTimestamps that dates each block’s bytes, are what make a rewrite detectable.
  • No API. Static files, cacheable, archivable, which anyone can mirror.
Blockchain specification
Checks

What anyone can check, from the register alone.

With no trust in the platform, using the published data only.

  1. 1 Every ballot is signed by a member of its ring. Re-run the ring-signature verification on (payload, election identifier, signature, key image, ring) for every ballot.
  2. 2 One key, one ballot. All key images of the election are distinct: no key was used twice. Since a voter may have received several keys, it takes checks 3 and 4 to turn this into one ballot per voter.
  3. 3 No ballot added to a ring. For every ring, the number of ballots equals the number of participation lines whose voted ring is that ring.
  4. 4 No more ballots than voters. Total ballots do not exceed the number of has_voted lines: this is the binding bound against ballot stuffing. In weighted voting, the signed weights and the voters’ roll weights form the same multiset, summing to voted_weight.
  5. 5 The recount. Serialize the payloads to CEF and tally them with CondorcetPHP or any CEF implementation, under the constraints derived from the published ballot rules: the same ranking and the same expressed, blank and null counts come out.
  6. 6 The elected. Read the top seats positions of the recomputed ranking.

What each voter can check on top

  • Their participation line. By their voter identifier: voted or not, number of keys received. Activity fabricated in their name shows.
  • Their ring. The ring digest in their receipt, computed by their own browser, must equal the one the register publishes.
  • Their ballot. With the key image in their receipt, they find their ballot and check its content byte for byte, even before it is published.

The core guarantee is the fixed list of signed ballots: checks 1 to 4, plus the integrity of the chain. The recount is a cross-check; a result divergence between implementations would be a bug in a calculation program, not a verifiability failure.

Verification procedure
Limits

What the register does not say.

What it withholds by design, and what it cannot prove or lets through.

What it does not reveal

  • No timestamps. No published record carries a date; operational datetimes stay in the database.
  • No arrival order. Ballots sorted by key image, the participation register by voter identifier: two different sort keys, unrelated to chronology.
  • No link between voter and ballot. The participation register is published only at close, never per window: its lines cannot be aligned in time with the ballots. Only non-anonymous voting creates that link, by the organizer’s choice.
  • No identity, by default. Without the nominative option the register holds no personal data; the mapping between identifier and identity is known only to the platform and the organizer.
  • No cosmetic presentation. Logo, candidate photos, thank-you message: neither published nor hashed.

What it cannot prove, or lets through

  • The moment of voting. That the server did not observe a voter while they voted: the platform is a trusted third party at that moment.
  • Fabricated participation. That a “voted” line was not fabricated for a no-show: only that voter can tell.
  • Who signed within the ring. By design.
  • A coarse order. Votes blocks follow one another: every ballot in a block was cast before the next one was sealed, at a granularity of at least 600 ballots. An emergency ring, published later, likewise signals that its ballots came later.
  • Sparse rings and rare weights. In a ring where a single voter voted, their identifier is linked to their ballot, and their name under the nominative option. The organizer, who knows the name behind every identifier, can make that link whatever the key mode. In anonymous weighted voting, a near-unique weight points to its voter.
  • Registered voters without a key. In an election where voters must bring their own key, a registered voter who never submitted one cannot vote and does not, today, appear in the published participation register.
  • Immutability of the page. The register page can change; immutability is checked on the blockchain, to the extent third parties kept copies of it.