Aller au contenu
Condorcet.vote Alpha
← Documentation technique

Blockchain

Tout ce dont un vérificateur indépendant a besoin pour lire, contrôler et redépouiller la chaîne d'une élection, sans rien nous demander.

Chaque élection publie son registre sous la forme d'une suite de fichiers JSON, chaînés par empreinte et signés par la plateforme. Cette page est la référence de ce format : l'enveloppe commune, les quatre types de blocs champ par champ, les règles à l'octet, et les schémas JSON contre lesquels la plateforme valide chaque bloc avant de le sceller, téléchargeables ci-dessous.

La version est liée à chaque élection pour toujours et répétée dans chaque bloc ; un changement de format passerait par une nouvelle version avec son propre jeu de schémas.

Protocole v1

Téléchargements

Les schémas JSON, tels que scellés.

JSON Schema draft 2020-12, un fichier par type de bloc plus l'enveloppe du bulletin signé. Ce sont exactement les fichiers que la plateforme utilise : aucune propriété additionnelle n'est jamais acceptée, et chacun est servi à l'URL canonique inscrite dans son $id.

Enveloppe

Cinq champs sur chaque bloc.

Quel que soit son type, un bloc s'ouvre sur la même enveloppe, dans cet ordre de lecture. L'empreinte et la signature du bloc ne sont jamais dans le JSON (un document ne peut contenir sa propre empreinte) : elles voyagent à côté.

protocol_version entier
Toujours 1 pour cette version ; identique sur chaque bloc d'une chaîne.
type chaîne
Un parmi configuration, rings, votes, closure.
election_id UUID v4, minuscules
L'identifiant de l'élection, le même sur chaque bloc, et le paramètre election_id de chaque signature en anneau.
block_number entier ≥ 0
Consécutif par élection, à partir de 0 pour le bloc de configuration.
previous_hash null ou 128 caractères hexa
SHA3-512 des octets stockés du bloc précédent, hexa minuscule ; null sur le bloc 0 seulement.
Enveloppe · block.*.json
{
  "protocol_version": 1,
  "type": "configuration | rings | votes | closure",
  "election_id": "0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21",
  "block_number": 0,
  "previous_hash": null,
  ...
}
Types de blocs

Quatre types, un cycle fixe.

Un bloc de configuration en tête, un bloc d'anneaux pour les anneaux de lancement, un bloc d'anneaux par anneau d'urgence créé pendant le vote, des blocs de votes par lots, et un unique bloc de clôture qui termine la chaîne. Le résultat n'est jamais un bloc : il se recalcule à partir des bulletins publiés.

Bloc 0 · genèse

configuration · la définition gelée

Ce qui ne changera plus une fois le vote ouvert. La présentation cosmétique (logo, photos, message de remerciement) est délibérément hors du bloc, non hachée.

title, description?, organizer_name chaînes
Contexte informatif, signé avec le bloc mais jamais une entrée du dépouillement.
election_type preferential | fptp
Classement (méthodes de Condorcet) ou choix unique. La méthode de Condorcet elle-même est publiée avec le résultat, pas ici.
seats entier ≥ 1
Sièges à pourvoir.
voter_access_mode chaîne
closed_nominative_list en version 1.
enrolment_mandatory booléen
Clés apportées par les votants uniquement (BYO-only) si vrai ; clés générées par le serveur autorisées si faux.
non_anonymous_voting, nominative_participation, weighted_voting booléens
Les trois options de vie privée et de pondération. non_anonymous_voting = true impose nominative_participation = true (imposé par le schéma).
max_ring_size entier ≥ 1
Le plafond de taille d'anneau de cette élection : aucun anneau ne le dépasse, chaque anneau d'urgence fait exactement cette taille.
initial_electoral_roll_size entier ≥ 0
La taille de la liste électorale à l'ouverture. Des votants peuvent être ajoutés pendant le vote ; la liste de clôture donne la taille finale, le nombre d'ajouts est donc public, jamais qui.
ballot_rules objet
ties_allowed, partial_ranking_allowed, blank_allowed, full_ranking_required : les paramètres de bulletin dont les contraintes de dépouillement sont dérivées de façon déterministe.
candidates[].label tableau de 2 à 30 objets
Étiquettes uniques, 1 à 64 caractères, triées par étiquette (ordre des octets). L'étiquette est le seul identifiant de candidat.
Bloc 1 · puis un par anneau d'urgence

rings · les clés autorisées à signer

La liste complète des clés publiques de chaque anneau. Le bloc 1 porte les anneaux de lancement ; tout bloc d'anneaux ultérieur porte un anneau d'urgence, ce qui se déduit de sa position. L'ensemble des membres d'un anneau est gelé pour toujours une fois publié.

rings[].ring_id UUID
L'identifiant d'anneau référencé par chaque bulletin signé contre lui ; anneaux triés par ring_id.
rings[].ring_digest ring_<64 hexa>_<8 hexa>
L'empreinte cryptoVote canonique de l'ensemble des membres de l'anneau, indépendante de l'ordre. Dérivable de public_keys, un vérificateur la recalcule ; elle est publiée pour qu'un votant puisse la comparer à celle que son navigateur a calculée à partir de l'anneau reçu.
rings[].public_keys[] pk_<64 hexa>_<8 hexa>
Clés publiques préfixées avec somme de contrôle, triées par ordre croissant, uniques ; au plus max_ring_size par anneau.

Les anneaux de lancement partagent une taille unique par élection (entre le plancher de 100 clés et le plafond) ; les anneaux d'urgence font exactement le plafond ; les anneaux d'une élection BYO-only contiennent ses inscrits, répartis équitablement.

Blocs suivants · par lots

votes · les bulletins signés

Les bulletins sont publiés par lots d'au moins 600 (le reliquat à la clôture), jamais un par un, et triés par image de clé au sein de chaque bloc, sans horodatage ni ordre d'arrivée.

ballots[].ring_id UUID
L'anneau dont les clés publiques permettent de vérifier la signature.
ballots[].key_image ki_<64 hexa>_<8 hexa>
L'image de clé à portée d'élection : unique par élection. Un second bulletin avec la même image est un double vote, publiquement manifeste. Clé de tri du bloc.
ballots[].signature blsag_…
La signature en anneau chaînable (variante bLSAG sur Ristretto255) sur les octets exacts de la charge utile.
ballots[].payload chaîne (JSON opaque)
Le document signé, tel quel : {"v":1,"election":"…","ballot":"A>B=C","weight":1[,"voter":"…"]}. Jamais re-sérialisé (voir le schéma de la charge utile).

Le champ ballot est une chaîne plate en syntaxe de classement CEF (ou la sentinelle /EMPTY_RANKING/). Son contenu n'est délibérément pas validé : une expression malformée est classée bulletin nul au dépouillement selon une règle publiée et reproductible.

Bloc final · terminateur

closure · totaux et registre d'émargement

Exactement un, le dernier, rien après lui. Détenir le bloc de clôture signé est la preuve qu'une chaîne est complète. Le registre d'émargement y apparaît une seule fois, jamais pendant le vote.

totals objet d'entiers
block_count, ballot_count, ring_count, voted_count, voted_weight, recoupables avec la chaîne elle-même.
electoral_roll[].voter_id UUID v4
L'identifiant de vote publié (un pseudonyme aléatoire) ; la liste à la clôture, triée par voter_id, une ligne par votant, aucune marquée comme ajoutée.
electoral_roll[].key_provisioning_mode chaîne
server_generated, organizer_imported ou voter_enrolled. Savoir si la plateforme a un jour détenu la clé de ce votant est un fait public.
electoral_roll[].ring_id UUID ou null
L'anneau dans lequel le votant a voté ; null s'il n'a pas voté. Le lien vérifiable votant → anneau.
electoral_roll[].distribution_count entier ≥ 0
Le nombre de clés distribuées au votant (3 au plus par défaut ; les rallonges accordées par l'organisateur restent visibles).
electoral_roll[].has_voted, weight booléen, entier ≥ 1
Drapeau de participation et poids de bulletin faisant autorité (1 sauf weighted_voting).
electoral_roll[].voter_name, email_digest chaînes, optionnelles
Divulgation nominative seulement, sur les votants ayant voté : nom en clair et SHA3-256(election_id ‖ e-mail normalisé).
Sérialisation

Les octets sont le contrat.

  • Stocké = servi = signé. Le fichier de bloc est écrit une fois et jamais re-sérialisé. Il n'y a pas d'étape de JSON canonique : ce qui est haché et signé est le fichier tel qu'il est servi.
  • JSON UTF-8, un élément par ligne. L'objet externe est indenté ; chaque élément des grandes collections (candidates, rings, ballots, electoral_roll) est un objet compact sur sa propre ligne. Barres obliques et Unicode ne sont pas échappés.
  • Tri déterministe, ordre des octets. candidates par label, public_keys croissant, ballots par key_image, electoral_roll par voter_id, rings par ring_id : comparaison brute des octets, jamais une collation linguistique.
  • Des entiers seulement, aucun horodatage. protocol_version, block_number, seats, tailles et totaux sont des entiers non négatifs ; toute autre valeur est une chaîne. Aucun bloc ne porte de date, aucun bulletin d'ordre d'arrivée.
  • Aucune propriété additionnelle, aucune clé dupliquée. Chaque schéma interdit les champs inconnus ; un vérificateur doit rejeter un bloc dont le JSON répète une clé, les analyseurs divergeant sur la valeur retenue.
Intégrité

Empreinte, signature, horodatage.

  • Chaînage SHA3-512. L'empreinte d'un bloc est le SHA3-512 de ses octets stockés, 128 caractères hexa minuscules ; c'est le previous_hash du bloc suivant. Recalculez-la avec openssl dgst -sha3-512.
  • Signature OpenPGP v6 détachée. Chaque bloc est signé sur ses octets exacts par la clé Ed25519 de la plateforme (RFC 9580), en signature détachée ASCII-armored dans le fichier .sig. Sa date de création est le signed_at du bloc ; une notation domain@condorcet.vote = chain-block/v1 empêche tout rejeu sur une autre classe d'artefact.
  • La rotation des clés reste vérifiable. Les certificats retirés restent publiés avec leur expiration : les règles de validité OpenPGP standard acceptent les blocs signés dans la fenêtre d'une clé et rejettent les autres.
  • Ancrage OpenTimestamps. Le SHA-256 simple de chaque bloc est engagé dans la blockchain Bitcoin ; le fichier .ots est servi en attente ou complété et se vérifie hors ligne avec l'outillage standard.
  • Des fichiers auto-identifiants. Les artefacts stockés portent l'identifiant de l'élection ({election}_{n}.json et fichiers compagnons), si bien qu'un fichier mis en miroir reste identifiable une fois détaché de son URL.
L'horodatage OpenTimestamps en détail
Endpoints

Des fichiers bruts, pas d'API.

Tout est servi sous forme de fichiers cachables et mirrorables sur le domaine principal, de l'ouverture du vote jusqu'à l'archivage de l'élection. Une élection annulée retire son registre.

/elections/{election}/chain
Le manifeste : liste ordonnée des blocs avec type, empreinte, signed_at, URL des artefacts et statut de l'horodatage.
/elections/{election}/chain/{n}.json
Le bloc n, les octets stockés exacts.
/elections/{election}/chain/{n}.json.sig
La signature OpenPGP détachée armored du bloc n.
/elections/{election}/chain/{n}.json.ots
La preuve OpenTimestamps du bloc n, en attente ou complétée.
/.well-known/blockchain-signing-key
Les certificats de signature de la plateforme, actifs et retirés, concaténés (application/pgp-keys).
/schemas/blockchain/block.{type}.schema.json
Les schémas JSON ci-dessus, à leurs URL canoniques $id.
La procédure de vérification complète, contrôle par contrôle