v3 : le vote par la chaîne Ğ1, côté moteur et serveur

Le bulletin scellé (ld1: + blake2b-256 du bulletin canonique, sel de 128 bits),
l'ordre de la chaîne (bloc, puis rang de l'événement : le dernier fait foi), les
adresses SS58 comparées par clé (ld-engine, pur). trustin-access : Monnaie libre
vote par la chaîne, les groupes gérés sur le serveur ; le droit de vote se lit à
la confirmation. Serveur : bulletins en attente, confirmation sur l'indexeur
(virement, auteur, identité, membre), identité idty:<index> créée au besoin,
tâche de fond toutes les 10 s, expiration à 24 h, vote groupé en une écriture
dans les groupes gérés. Migration additive 0006. Sans LD_VOTE_ACCOUNT, le vote
par la chaîne est éteint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-30 20:52:47 +01:00
co-authored by Claude Opus 5.5
parent a576b68570
commit 3541e1b1a6
19 changed files with 2595 additions and 105 deletions
+65 -16
View File
@@ -14,7 +14,9 @@
//! - everything is open to a member except access management, which belongs
//! to trustinWallet's administrators;
//! - where the electorate is the web of trust (Monnaie libre), only its members
//! vote (2026-10-21, independent review: one person, one vote).
//! vote (2026-10-21, independent review: one person, one vote) — through the
//! Ğ1 chain, one transaction per vote, for the identity that signs it, member
//! when the vote is confirmed (2026-10-30, ADR 0008).
use serde::{Deserialize, Serialize};
@@ -101,16 +103,46 @@ pub fn opens(identity: &Identity, group: &Group, granted: bool) -> bool {
is_member(identity, group, granted)
}
/// Can this identity vote in the group? Where the electorate is the Ğ1 web of
/// trust (policy `G1Members`), only its members vote: the threshold is computed
/// on the size of the web, and an invitation identity next to a Ğ1 identity
/// would give one person two votes. Development identities stand in for members
/// (local only: the dev login is never enabled in production).
/// How a group votes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VoteWay {
/// On the server: one vote per identity of the group (managed groups).
Server,
/// On the Ğ1 chain: one transaction per vote, signed by the person's wallet
/// (ADR 0008). The vote counts for the Ğ1 identity of the signer.
Chain,
}
/// Where the electorate is the Ğ1 web of trust (policy `G1Members`), votes go
/// through the chain (Yvv, 2026-10-30: « une transaction par vote »): one
/// person, one voice, proven by the identity that signs — not by the way one
/// signed in here. Elsewhere, on the server.
pub fn vote_way(group: &Group) -> VoteWay {
match group.policy {
GroupPolicy::G1Members => VoteWay::Chain,
GroupPolicy::Managed => VoteWay::Server,
}
}
/// Can this identity vote on the server in the group? In a managed group, every
/// member. Where the vote goes through the chain, nobody: an invitation identity
/// next to a Ğ1 identity would give one person two voices.
pub fn votes_in(identity: &Identity, group: &Group, granted: bool) -> bool {
opens(identity, group, granted)
&& (group.policy != GroupPolicy::G1Members
|| identity.kind == IdentityKind::Dev
|| (identity.kind == IdentityKind::G1 && identity.g1_member))
opens(identity, group, granted) && vote_way(group) == VoteWay::Server
}
/// Can this identity ask for a ballot to sign with a wallet? Whoever opens a
/// group that votes on the chain: who votes is decided by the chain itself, at
/// confirmation (`chain_vote_counts`).
pub fn asks_ballot(identity: &Identity, group: &Group, granted: bool) -> bool {
opens(identity, group, granted) && vote_way(group) == VoteWay::Chain
}
/// Does a vote signed on the chain count? Only for an identity that is a member
/// of the web of trust when the vote is confirmed (the electorate W is the web).
pub fn chain_vote_counts(member: bool) -> bool {
member
}
/// Access management stays with trustinWallet's administrators.
@@ -191,15 +223,32 @@ mod tests {
}
#[test]
fn only_wot_members_vote_where_the_electorate_is_the_web_of_trust() {
fn where_the_electorate_is_the_web_of_trust_the_vote_goes_through_the_chain() {
let ml = group("monnaie-libre");
assert!(votes_in(&who(IdentityKind::G1, true, false), &ml, false));
assert_eq!(vote_way(&ml), VoteWay::Chain);
assert_eq!(vote_way(&group("essai")), VoteWay::Server);
// Nobody votes on the server there, whatever the way one signed in.
for kind in [IdentityKind::G1, IdentityKind::Invite, IdentityKind::Dev] {
assert!(!votes_in(&who(kind, true, false), &ml, true), "{kind:?}");
}
// Whoever opens the group may ask for a ballot to sign with a wallet...
let invited = who(IdentityKind::Invite, false, true);
assert!(opens(&invited, &ml, true), "an invitation may open Monnaie libre to read and propose");
assert!(!votes_in(&invited, &ml, true), "but its votes would not be web-of-trust votes");
assert!(!votes_in(&who(IdentityKind::G1, false, false), &ml, true), "a former member reads, does not vote");
assert!(votes_in(&invited, &group("essai"), true), "managed groups: every member votes");
assert!(!votes_in(&invited, &group("essai"), false));
assert!(asks_ballot(&invited, &ml, true));
assert!(asks_ballot(&who(IdentityKind::G1, true, false), &ml, false));
assert!(!asks_ballot(&invited, &ml, false), "not without opening the group");
// ...and the vote counts only for a member of the web of trust, read on the chain.
assert!(chain_vote_counts(true));
assert!(!chain_vote_counts(false), "a former member, or an account without identity, does not vote");
}
#[test]
fn in_a_managed_group_every_member_votes_on_the_server() {
let essai = group("essai");
let invited = who(IdentityKind::Invite, false, false);
assert!(votes_in(&invited, &essai, true), "managed groups: every member votes");
assert!(!votes_in(&invited, &essai, false));
assert!(!asks_ballot(&invited, &essai, true), "no chain there: the test group stays easy to try");
}
#[test]