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
+50 -1
View File
@@ -21,6 +21,39 @@ pub struct Publish {
pub author_email: String,
}
/// The vote on the Ğ1 chain (ADR 0008): where the 0.1 Ğ1 of each vote go, how
/// much, and the nodes the page sends the transaction to. Without
/// `LD_VOTE_ACCOUNT`, voting through the chain is off, and the interface says so.
#[derive(Clone, Debug)]
pub struct VoteChain {
/// The vote account, as a Ğ1 address (`g1…`), whatever form it was given in.
pub account: String,
pub account_key: [u8; 32],
/// Cents of Ğ1 per vote (`LD_VOTE_AMOUNT`, 10 = 0.1 Ğ1 by default).
pub amount: u64,
/// WebSocket endpoints of Ğ1 nodes, for the page (`LD_VOTE_NODES`).
pub nodes: Vec<String>,
/// Tests only, with the dev login: the page sends to this fake node instead
/// of building the transaction with polkadot-api (`LD_VOTE_TEST_NODE`).
pub test_node: Option<String>,
}
impl VoteChain {
pub const DEFAULT_AMOUNT: u64 = 10;
/// From the account in any SS58 form; `None` if it is not an address.
pub fn new(account: &str, amount: u64, nodes: Vec<String>, test_node: Option<String>) -> Option<Self> {
let (_, key) = ld_engine::address::decode(account.trim())?;
Some(Self {
account: ld_engine::address::encode(ld_engine::address::G1_PREFIX, &key),
account_key: key,
amount: amount.max(1),
nodes,
test_node,
})
}
}
#[derive(Clone, Debug)]
pub struct Config {
pub bind: String,
@@ -38,6 +71,8 @@ pub struct Config {
pub indexers: Vec<String>,
pub wot_fallback: u32,
pub build: String,
/// The vote on the chain; `None` = off (no `LD_VOTE_ACCOUNT`).
pub vote: Option<VoteChain>,
}
fn var(name: &str) -> Option<String> {
@@ -76,6 +111,18 @@ impl Config {
author_name: var("LD_PUBLISH_AUTHOR").unwrap_or_else(|| "libreDecision".into()),
author_email: var("LD_PUBLISH_EMAIL").unwrap_or_else(|| "decision@librodrome.org".into()),
});
let dev_login = flag("LD_DEV_LOGIN");
let vote = var("LD_VOTE_ACCOUNT").and_then(|account| {
let amount = var("LD_VOTE_AMOUNT").and_then(|v| v.parse().ok()).unwrap_or(VoteChain::DEFAULT_AMOUNT);
let nodes = list("LD_VOTE_NODES", &["wss://rpc.duniter.org", "wss://g1.axiom-team.fr/ws"]);
// A fake node for the page, never outside the local dev login.
let test_node = var("LD_VOTE_TEST_NODE").filter(|_| dev_login);
let chain = VoteChain::new(&account, amount, nodes, test_node);
if chain.is_none() {
tracing::error!("LD_VOTE_ACCOUNT is not a Ğ1 address: the vote through the chain stays off");
}
chain
});
Self {
bind: var("LD_BIND").unwrap_or_else(|| "0.0.0.0:8002".into()),
web_dir: PathBuf::from(var("LD_WEB_DIR").unwrap_or_else(|| "web/build".into())),
@@ -86,7 +133,7 @@ impl Config {
.to_owned(),
connect,
admin_g1_ids: list("LD_ADMIN_G1_IDS", &[]),
dev_login: flag("LD_DEV_LOGIN"),
dev_login,
bootstrap_admin: flag("LD_BOOTSTRAP_ADMIN"),
publish,
rpc_endpoints: list(
@@ -104,6 +151,7 @@ impl Config {
),
wot_fallback: var("LD_WOT_FALLBACK").and_then(|v| v.parse().ok()).unwrap_or(5745),
build: var("LD_BUILD").unwrap_or_else(|| "dev".into()),
vote,
data_dir,
}
}
@@ -134,6 +182,7 @@ impl Config {
indexers: vec![],
wot_fallback: 5745,
build: "test".into(),
vote: None,
}
}
}