Verification procedure
Two paths to check an election without trusting the platform: the published register is enough to verify the ballots and the result; the blockchain adds the integrity and immutability of that register.
The second path includes the first. Each check states what it proves and links the specification it relies on; the tools are standard or open source: CryptoVote, CondorcetPHP, sq, openssl, ots.
Protocol v1
The ballots and the result, from the register.
An election’s public register publishes its definition, its rings, its signed ballots and, at the close, its participation register. These checks take them as served, on the register page or in the block files. The participation checks wait for the close: the participation register is published once, in the closure block.
-
1
Every ballot is authentic.
For every ballot, run verify_vote (CryptoVote operation C) on the payload as published, the election id, the signature, the key image and the public keys of its ring. Also recompute each ring’s digest (ring_digest) from its public_keys and compare it with the published one.
What it proves: Every ballot was signed by a key of its ring, over these exact bytes: a modified ballot, or one signed outside the ring, does not verify.
Relies on Signed ballot
-
2
Once per key.
All key images (ki_…) are distinct within the election. A key image is specific to the secret key and the election: two ballots signed with the same key carry the same image.
What it proves: No key was used twice: such a double vote would be publicly evident.
Relies on Signed ballot
-
3
Each ring matches its voters.
For every ring R, the number of ballots signed against R equals the number of participation lines whose voted ring (ring_id) is R. The bound “ballots ≤ keys in the ring” follows from key-image uniqueness.
What it proves: Each ring’s ballots tie to voters recorded as having voted in that ring.
Relies on Public register
-
4
No more ballots than voters.
The total number of ballots does not exceed the number of has_voted lines in the participation register. In a weighted election, the multiset of the weights signed in the ballots equals the multiset of the weights on the has_voted lines.
What it proves: This is the bound that prevents ballot stuffing: the platform can publish neither more ballots than voters marked as having voted, nor a weight no voter was granted, and no ballot is ever linked to a voter.
Relies on Public register
-
5
The result can be recomputed.
Convert every published payload to CEF, with its signed weight in a weighted election, and feed it to CondorcetPHP (or any CEF-compliant implementation) with the constraints derived from the definition’s ballot rules. The published result names the method, the library version and the tally contract version; the elected candidates are the top of the ranking, as many as there are seats.
What it proves: The announced ranking and counts (expressed, blank, null) are those the published ballots yield. The core guarantee remains the signed list of ballots: a divergence between implementations would be a calculation bug, not a verifiability failure.
Relies on Tally & recount
-
6
Every voter finds their ballot.
With their receipt (.condorcet_receipt), the voter looks up their key image on the register page, which also finds a ballot received but not yet published, and compares the published ballot, byte for byte, with the receipt’s signed payload. They also compare the receipt’s ring_digest, computed by their own browser, with the one published for the ring, and check their participation line by their voter ID.
What it proves: Their ballot appears exactly as they signed it, their ring is the one published for everyone, and no activity was fabricated on their line. On their own initiative, they can also prove to a third party that a published ballot is theirs, which reveals their vote.
Relies on Voter files Vote proof
Integrity and immutability, checkable by anyone.
The register is also published as chained JSON blocks, signed by the platform and anchored in Bitcoin. Seven checks, against the published files alone, establish that this register is authentic, complete and unchanged; the sixth replays path 1 on it. The platform provides no verifier: a self-check by the party that produces the chain would add no trust.
-
1
Fetch the files.
The platform’s certificate set at /.well-known/blockchain-signing-key, retired keys and expirations included; the election’s manifest (/elections/{election}/chain); every block {n}.json, its signature {n}.json.sig and its proof {n}.json.ots.
What it proves: That no privileged access is needed: these are raw files, cacheable and mirrorable, named with the election id, so a copy detached from its URL stays identifiable.
Relies on Platform keys Blockchain
-
2
Verify every signature.
One command per block, with any RFC 9580 implementation: sq verify, since GnuPG cannot verify OpenPGP v6 signatures. The standard validity rules reject a signature dated outside its key’s window. For strict verification, require the notation domain@condorcet.vote = chain-block/v1 and a creation time equal to the manifest’s signed_at.
What it proves: Authenticity: every block was produced by the platform and has not changed by a single byte since it was signed. Not, on its own, immutability (see the limits).
Relies on Platform keys
-
3
Check the chaining.
Block 0 is a configuration block with a null previous_hash; for every block k > 0, the SHA3-512 of file k − 1 (openssl dgst -sha3-512) equals block k’s previous_hash; block_number runs consecutively from 0 to n; election_id is the same on every block.
What it proves: The order and continuity of the chain: no block can be removed, inserted or modified without breaking the chaining of every later block.
Relies on Blockchain
-
4
Check the closure.
Exactly one closure block, last, nothing after it. Its totals cross-check with the chain: block_count with the number of blocks (closure included), ballot_count with the ballots of the votes blocks, ring_count with the rings of the rings blocks, voted_count and voted_weight with the has_voted lines of its participation register.
What it proves: The chain is complete: a signed closure block at the end of an unbroken chain proves no block is missing.
Relies on Blockchain
-
5
Validate every block.
Each block validates against the JSON Schema of its type (/schemas/blockchain/block.{type}.schema.json), which rejects any additional property; a block whose JSON repeats a key is rejected.
What it proves: Every verifier reads the same content: since parsers disagree on duplicate keys, such a block could read as two different ballot sets under the same digest and the same signature.
Relies on Blockchain
-
6
Replay path 1 on the chain.
On the blocks’ contents: every ring signature against its ring’s keys, every ring_digest recomputed from its public_keys, key-image uniqueness, the per-ring and global bounds against the closure block’s participation register, then the recount from the configuration block’s rules and the votes blocks’ payloads.
What it proves: The guarantees of path 1, now on a register whose authenticity, order and completeness are established.
Relies on Public register Tally & recount
-
7
Verify the Bitcoin anchoring.
Optionally, each .ots proof against its block file, with standard OpenTimestamps tooling (ots verify). The commitment is the file’s plain SHA-256. A pending proof only carries the calendar commitments so far; a completed proof points to a Bitcoin block.
What it proves: Prior existence: the block existed, byte for byte, no later than the attested Bitcoin block, independently of the platform’s keys, since the platform cannot back-date a Bitcoin block.
Relies on Blockchain
# the published keyring, active and retired keys
curl -s https://admin.stg.condorcet.vote/.well-known/blockchain-signing-key > signing-keys.pgp
# for each block n: the OpenPGP v6 signature (Sequoia; GnuPG cannot read v6)
sq verify --signer-file signing-keys.pgp --signature-file {n}.json.sig {n}.json
# the chaining: the digest must equal block n + 1's previous_hash
openssl dgst -sha3-512 {n}.json
# optionally, the Bitcoin anchoring
ots verify -f {n}.json {n}.json.ots
What these paths do not prove.
Both paths verify what was published. They do not replace the trust placed in the platform while the vote is running.
- Path 1 takes the register as served. It does not tell whether that register is complete or was rewritten: the register page is not immutable. That is the job of path 2.
- A signature is not immutability. The platform holds its key and could sign a consistent rewritten chain. What keeps a rewrite from going unnoticed: the copies kept by anyone who fetches the blocks as they are published, and the OpenTimestamps anchoring, since a rewritten block cannot be anchored before it was rewritten.
- A trusted third party at vote time. No register proves that the platform did not observe voters while the vote was running. The “bring your own key” mode reduces what you have to entrust to it.
- Participation fabricated for a no-show. The platform could mark a voter who did not vote as having voted, and vote in their place within the published bounds; only that voter can detect it, by checking their participation line.
- Who signed within a ring. By design, the ring signature does not reveal which member signed. A voter can, however, prove their own vote: the protocol offers no coercion resistance.
See also
- Public register What every election publishes, whatever carries it: definition, rings, participation register, ballots.
- Blockchain The over-protocol that seals the register: envelope, four block types, serialization, chaining, signature, timestamping.
- Platform keys The OpenPGP v6 certificates that sign the blocks, their rotation and their machine endpoint.