forked from yvv/decision
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:
@@ -0,0 +1,160 @@
|
||||
//! Ğ1 account addresses (SS58).
|
||||
//!
|
||||
//! The same public key reads `g1…` on the Ğ1 chain (prefix 4450, what the
|
||||
//! indexer and Ğecko show) and `5…` in the generic Substrate form (what
|
||||
//! duniter-connect shows). Accounts are compared by key, never by text.
|
||||
|
||||
/// The SS58 prefix of the Ğ1 chain.
|
||||
pub const G1_PREFIX: u16 = 4450;
|
||||
|
||||
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
fn base58_decode(s: &str) -> Option<Vec<u8>> {
|
||||
// Big-endian bytes of the number, grown symbol by symbol.
|
||||
let mut number: Vec<u8> = Vec::new();
|
||||
for c in s.bytes() {
|
||||
let mut carry = u32::try_from(ALPHABET.iter().position(|a| *a == c)?).ok()?;
|
||||
for b in number.iter_mut().rev() {
|
||||
carry += u32::from(*b) * 58;
|
||||
*b = (carry & 0xff) as u8;
|
||||
carry >>= 8;
|
||||
}
|
||||
while carry > 0 {
|
||||
number.insert(0, (carry & 0xff) as u8);
|
||||
carry >>= 8;
|
||||
}
|
||||
}
|
||||
let mut out = vec![0u8; s.bytes().take_while(|c| *c == b'1').count()];
|
||||
out.extend(number);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn base58_encode(data: &[u8]) -> String {
|
||||
// Little-endian base-58 symbols.
|
||||
let mut symbols: Vec<u8> = Vec::new();
|
||||
for byte in data {
|
||||
let mut carry = u32::from(*byte);
|
||||
for d in symbols.iter_mut() {
|
||||
carry += u32::from(*d) << 8;
|
||||
*d = (carry % 58) as u8;
|
||||
carry /= 58;
|
||||
}
|
||||
while carry > 0 {
|
||||
symbols.push((carry % 58) as u8);
|
||||
carry /= 58;
|
||||
}
|
||||
}
|
||||
let zeros = data.iter().take_while(|b| **b == 0).count();
|
||||
std::iter::repeat_n('1', zeros).chain(symbols.iter().rev().map(|d| char::from(ALPHABET[usize::from(*d)]))).collect()
|
||||
}
|
||||
|
||||
/// The two bytes that close an address: blake2b-512 of `SS58PRE`, the prefix and the key.
|
||||
fn checksum(payload: &[u8]) -> [u8; 2] {
|
||||
use blake2::{Blake2b512, Digest};
|
||||
let mut h = Blake2b512::new();
|
||||
h.update(b"SS58PRE");
|
||||
h.update(payload);
|
||||
let out = h.finalize();
|
||||
[out[0], out[1]]
|
||||
}
|
||||
|
||||
fn prefix_bytes(prefix: u16) -> Vec<u8> {
|
||||
if prefix < 64 {
|
||||
vec![prefix as u8]
|
||||
} else {
|
||||
// Two bytes for the prefixes 64 to 16383, as Substrate writes them.
|
||||
vec![
|
||||
(((prefix & 0b1111_1100) >> 2) as u8) | 0b0100_0000,
|
||||
((prefix >> 8) as u8) | (((prefix & 0b11) as u8) << 6),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefix and the 32-byte public key of an SS58 address, if it is one.
|
||||
pub fn decode(addr: &str) -> Option<(u16, [u8; 32])> {
|
||||
let data = base58_decode(addr)?;
|
||||
let (prefix, at) = match *data.first()? {
|
||||
b @ 0..=63 => (u16::from(b), 1),
|
||||
b @ 64..=127 => {
|
||||
let b1 = *data.get(1)?;
|
||||
let lower = ((b & 0b0011_1111) << 2) | (b1 >> 6);
|
||||
let upper = b1 & 0b0011_1111;
|
||||
(u16::from(lower) | (u16::from(upper) << 8), 2)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
if data.len() != at + 32 + 2 {
|
||||
return None;
|
||||
}
|
||||
let (payload, sum) = data.split_at(at + 32);
|
||||
if checksum(payload) != sum {
|
||||
return None;
|
||||
}
|
||||
Some((prefix, payload[at..].try_into().ok()?))
|
||||
}
|
||||
|
||||
/// The SS58 address of a public key under a prefix (up to 16383).
|
||||
pub fn encode(prefix: u16, key: &[u8; 32]) -> String {
|
||||
let mut data = prefix_bytes(prefix & 0x3fff);
|
||||
data.extend_from_slice(key);
|
||||
let sum = checksum(&data);
|
||||
data.extend_from_slice(&sum);
|
||||
base58_encode(&data)
|
||||
}
|
||||
|
||||
/// The Ğ1 form of an address in any SS58 form.
|
||||
pub fn to_g1(addr: &str) -> Option<String> {
|
||||
decode(addr).map(|(_, key)| encode(G1_PREFIX, &key))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn hex32(s: &str) -> [u8; 32] {
|
||||
let bytes: Vec<u8> = (0..64).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex")).collect();
|
||||
bytes.try_into().expect("32 bytes")
|
||||
}
|
||||
|
||||
// Computed with @polkadot/util-crypto (decodeAddress / encodeAddress), 2026-10-30.
|
||||
const VECTORS: [(&str, &str, &str); 3] = [
|
||||
(
|
||||
"g1NrqKgJW9FoFabaYW82zhS9tDWdQcL1DGpivaJdj7GW9M6iv",
|
||||
"aba6b0dcf21a0ddff2e87b52225ddddf06e8c83afb7cd874e7438dd9aa7c2ee8",
|
||||
"5FwmZc7BSxiLyBDCuB6U6oW9sQNmXk5vNRRYEy8qXxo2FAkn",
|
||||
),
|
||||
(
|
||||
"g1LQM6MfnjdPTrgz3ubwhwnPdbWUUq2GGQCqRQuGgrke9vRMA",
|
||||
"3efad05b01361fe102ce8bff5e5fd1be7554af684c7e741aaab256cdf950b196",
|
||||
"5DVHLHUU3LJZFGciJf1BM9juFQDqkSLyVoY35ZmoHSw2pZ1s",
|
||||
),
|
||||
(
|
||||
"g1Pn1geRZnHcVdWdKkoYwQrAARDTXK1D7H3zuXj7GNayT1thu",
|
||||
"d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
|
||||
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn g1_and_generic_forms_of_the_same_key() {
|
||||
for (g1, key, generic) in VECTORS {
|
||||
assert_eq!(decode(g1), Some((G1_PREFIX, hex32(key))), "{g1}");
|
||||
assert_eq!(decode(generic), Some((42, hex32(key))), "{generic}");
|
||||
assert_eq!(encode(G1_PREFIX, &hex32(key)), g1);
|
||||
assert_eq!(encode(42, &hex32(key)), generic);
|
||||
assert_eq!(to_g1(generic).as_deref(), Some(g1));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_is_not_an_address() {
|
||||
let (g1, _, _) = VECTORS[0];
|
||||
// One character changed: the checksum no longer holds.
|
||||
let mut tampered = g1.to_owned();
|
||||
tampered.replace_range(10..11, if &g1[10..11] == "a" { "b" } else { "a" });
|
||||
for bad in ["", "g1", "0OIl", &tampered, &g1[..g1.len() - 1], "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQ"]
|
||||
{
|
||||
assert_eq!(decode(bad), None, "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user