# 0008 — Le vote par transaction sur la chaîne Ğ1 > Complète [0002](0002-vote-permanent.md) (vote permanent) et [0006](0006-acces-connect-trustinwallet.md) > (accès). Décision d'Yvv du 2026-10-30 (revue, L9) ; le format du bulletin, la voie du > portefeuille et les réglages sont les choix par défaut d'orch (Q5, Q6, Q7), isolés pour être > changés sans toucher au reste. **Contexte.** Jusqu'ici, un vote était une ligne écrite par le serveur pour la session connectée. Dans Monnaie libre, l'électorat W est la toile de confiance, mais la connexion Ğ1 (connect.monnaie-libre.fr) n'est pas active et toutes les identités viennent d'invitations : en pratique, personne ne pouvait voter. Yvv, 2026-10-30 : « une personne une voix, mais la voix peut s'exprimer plusieurs fois […] c'est le dernier qui fait foi. Pour garantir l'intégrité d'un vote, je crois qu'il n'y a que la blockchain, donc une transaction par vote » ; « 0,1 Ğ1 » ; « qu'un geste dans le vote » ; « Ğ1Companion ou Ğecko, à terme trustinWallet » ; « Je peux voter tout d'un coup, ou une section entière, ou un article avec son titre, ou séparément. » ## Décision ### Qui vote où - **Où l'électorat est la toile** (politique `g1-members` : Monnaie libre), chaque vote est **une transaction de la chaîne Ğ1**, signée par le portefeuille de la personne. Le serveur n'y écrit plus de vote direct (`409 chain_only`). La voix est celle de **l'identité Ğ1 du compte qui signe**, quelle que soit la session qui a préparé le vote (invitation, plus tard connexion Ğ1). - **Dans un groupe géré** (essai, Axiom…), le vote reste écrit par le serveur, sans chaîne ni portefeuille, comme avant (`409 server_vote` si on y demande un bulletin). Le geste groupé y marche aussi : une seule écriture. - La règle est dans trustinWallet : `trustin_access::vote_way` (`chain` / `server`), `votes_in`, `asks_ballot`, `chain_vote_counts`. ### Le bulletin scellé (Q5) — `crates/ld-engine/src/ballot.rs` Le serveur scelle un **bulletin en attente** : le document, le compte qui signera (quand la page le connaît), les lignes (version, révision votée, choix), un sel de 128 bits tiré au hasard. Sa forme canonique, en ASCII, les lignes triées par version : ``` ld1 doc author line … salt <32 chiffres hexadécimaux> ``` `H = blake2b-256(forme canonique)` ; la **remarque** de la transaction est `ld1:` suivi des 64 chiffres hexadécimaux de H, 68 octets ASCII. Exemple (vecteur des tests, calculé à part) : ``` ld1 doc d0c author - line p1 3 for line p2 1 against salt 00112233445566778899aabbccddeeff ``` → remarque `ld1:20448d496744292981c8586856afb58a86e37da2470f764b4e6ed84eade71340`, que l'indexeur retrouve par `blake2b-256(remarque)` = `0xe765221a75716dde863a7b53df3a97acf6db38a2118edd525d54c8a2979b7bc3`. Le sel empêche de deviner le choix (trois choix par ligne se devineraient tous). L'adresse liée empêche qu'un autre compte, qui recopierait la remarque, prenne le vote (voir les limites). ### La transaction (Q6) — `web/src/lib/wallet/` ``` utility.batchAll([ balances.transferKeepAlive(LD_VOTE_ACCOUNT, LD_VOTE_AMOUNT), system.remarkWithEvent("ld1:" + H), ]) ``` Un seul geste, atomique : le virement et la remarque passent ensemble ou pas du tout. La page la fait signer par l'extension de portefeuille du navigateur (interface standard `injectedWeb3` / `signPayload` : duniter-connect aujourd'hui, Ğ1Companion ou trustinWallet le jour où elles l'offrent), avec **polkadot-api chargé au seul moment de signer**, et l'envoie à un nœud Ğ1 (`LD_VOTE_NODES`). Sans portefeuille dans le navigateur : **Ğecko**, par un lien et un QR code `g1://?amount=0.1&comment=ld1:` (un virement commenté, ce que Ğecko sait faire) ; ce bulletin-là n'est lié à aucun compte, et la page attend. ### La confirmation — `crates/ld-server/src/ballots.rs`, `indexer.rs` Le serveur lit l'indexeur (duniter-squid, `LD_INDEXERS`) : ```graphql query Ballot($hash: String!) { txComments(condition: {hash: $hash}, orderBy: BLOCK_NUMBER_ASC, first: 20) { nodes { id blockNumber remark author { id identity { index name isMember } } transfersByCommentId { nodes { fromId toId amount } } } } } ``` `hash` = `0x` + blake2b-256 des octets de la remarque. Il garde la première occurrence, dans l'ordre de la chaîne, qui ne vient pas d'un autre compte que celui lié, puis exige : 1. la remarque exacte ; 2. dans la même transaction, un virement **du signataire vers le compte des votes**, d'au moins le montant ; 3. une identité Ğ1 au signataire (`no_identity` sinon) ; 4. que cette identité soit **membre maintenant** (`not_member` sinon) — par la règle de trustinWallet. Le vote compte alors pour `idty:` (créée si besoin : sorte g1, membre), ligne par ligne, avec le bloc et le rang de l'évènement ; le balayage d'adoption tourne aussitôt. La session qui a demandé le bulletin voit ces votes comme « ton vote » — et seulement ceux de ses bulletins : une autre session ne voit jamais ce qu'a voté la même identité ailleurs. Le serveur relit l'indexeur pour chaque bulletin en attente toutes les 10 secondes (tâche de fond) et quand la page demande (au plus une lecture toutes les 3 secondes par bulletin). Un bulletin non vu en **24 heures** est lâché. Cas d'erreur, chacun testé : indexeur muet (le bulletin reste en attente), signataire non membre ou sans identité (refusé, et dit), mauvais montant ou mauvais destinataire (refusé), déjà confirmé (rien ne change), révision changée entre temps (le vote porte sur la révision votée : « à reconfirmer »). ### Le dernier fait foi Par version et par identité, **dans l'ordre de la chaîne** : le bloc, puis le rang de l'évènement (lu dans l'identifiant de l'évènement). Un bulletin confirmé tard ne remplace jamais un bulletin plus récent sur la chaîne. « Retirer mon vote » est un bulletin dont la ligne vaut `none`. Les transactions d'un même compte gardent l'ordre de ses envois (le nonce) ; le désordre ne vient que de l'ordre de lecture de l'indexeur, et la règle l'absorbe. Un bulletin que le portefeuille n'a pas signé (refus, fonds insuffisants, compte absent) est lâché aussitôt par la page (`DELETE /api/ballots/{id}` : `expired`, raison `dropped`), pour ne pas rester « en attente » une journée. Jamais un bulletin confirmé. ### Le vote groupé Un panier : article par article ; « tout pour » / « tout contre » pour une section ou tout le document — jamais pour un article dont plusieurs versions sont en concurrence : l'interface le montre, on choisit sa version. Puis **un geste** : une écriture (groupe géré), ou **une transaction** (chaîne) pour toutes les lignes, au même prix qu'une seule (0,1 Ğ1 par transaction, pas par ligne ; 200 lignes au plus, 20 bulletins en attente par session). ### Ce que voit la chaîne Qu'un compte (et son identité) a envoyé 0,1 Ğ1 au compte des votes avec une remarque `ld1:…`, à tel bloc : **qui a voté et quand, jamais quoi** — ni le document, ni le nombre de lignes, ni les choix. Le solde du compte des votes est public : il dit combien de votes ont été envoyés. ### Ce que sait le serveur Les bulletins, leurs lignes, leurs sels, la session qui les a demandés et l'identité qui les a signés : il sait qui a voté quoi, comme pour un vote direct, et ne montre que les totaux. Il ne peut pas inventer un vote d'une identité (il faudrait sa signature sur la chaîne) ; il pourrait en taire un. Chaque vote compté se vérifie contre la chaîne en recalculant H depuis les lignes et le sel — ce que personne d'autre ne peut faire tant que les sels restent au serveur. ### Les réglages (Q7) — `crates/ld-server/src/config.rs` - `LD_VOTE_ACCOUNT` : le compte des votes, une adresse Ğ1 (toute forme SS58, ramenée au préfixe g1). **Sans lui, le vote par la chaîne est fermé** et l'interface le dit avec des mots simples ; les groupes gérés votent comme avant. Une adresse illisible est journalisée et laisse le vote fermé. Aucune adresse n'est écrite en dur. - `LD_VOTE_AMOUNT` : en centimes de Ğ1, 10 par défaut (0,1 Ğ1), au moins 1. - `LD_VOTE_NODES` : les nœuds où la page envoie la transaction (par défaut `wss://rpc.duniter.org`, `wss://g1.axiom-team.fr/ws`). - `LD_VOTE_TEST_NODE` : tests seulement, et seulement avec la connexion de développement : la page envoie ce que le faux portefeuille a signé à un faux nœud, sans polkadot-api. ## Les données Migration `0006`, additive : `ballots` (le bulletin, son sel, H, `0x`+blake2b-256 de la remarque, le compte et le montant du moment, l'état `pending | confirmed | refused | expired`, la raison, l'identité qui a voté, le signataire, le bloc, le rang, l'évènement, ce qui est entré en vigueur, les dates) et `ballot_lines` (version, révision, choix). `votes` gagne `source` (`server` par défaut : tous les votes d'avant restent tels quels), `block`, `event_index`, `ballot_id`. ## Coût - Voter coûte 0,1 Ğ1 par geste, plus les frais de la chaîne (non mesurés ici) ; le portefeuille doit garder au moins 1 Ğ1 (dépôt existentiel, `transferKeepAlive`). Le compte des votes accumule ces Ğ1 : à qui il appartient, et ce qu'on en fait, reste à décider par Yvv. - Le serveur **fait confiance à l'indexeur** pour la remarque, le virement, l'identité et l'appartenance à la toile : un indexeur menteur ferait compter ou refuser un vote. Plusieurs indexeurs se configurent ; lire les évènements sur un nœud serait l'étape suivante. - Le droit de vote se vérifie **au moment de la confirmation sur la chaîne** : un membre sorti de la toile ensuite garde ses voix. - Par Ğecko, le bulletin n'est lié à aucun compte : un autre compte qui recopierait la remarque et la ferait inclure avant (en payant 0,1 Ğ1) prendrait ce bulletin — ses lignes compteraient pour lui. Par l'extension, le bulletin est lié au compte et la copie est ignorée. - Un paiement Ğecko fait après 24 heures ne compte pas (il est perdu pour le vote). - polkadot-api : environ 180 Ko (66 Ko compressés) chargés au moment de signer seulement ; `node_modules` grossit d'environ 77 Mo (son outillage). - La chaîne montre la participation de chacun (qui a voté, quand, combien de fois).